001package com.nimbusds.common.ldap;
002
003
004import java.io.File;
005import java.io.IOException;
006import java.util.Properties;
007
008import javax.servlet.ServletContext;
009import javax.servlet.ServletContextEvent;
010import javax.servlet.ServletContextListener;
011
012import com.unboundid.ldap.listener.InMemoryDirectoryServer;
013import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
014import com.unboundid.ldap.listener.InMemoryListenerConfig;
015import com.unboundid.ldap.sdk.LDAPException;
016import com.unboundid.ldap.sdk.OperationType;
017import com.unboundid.ldap.sdk.schema.Schema;
018import com.unboundid.ldif.LDIFException;
019
020import org.apache.log4j.Logger;
021
022import com.thetransactioncompany.util.PropertyParseException;
023import com.thetransactioncompany.util.PropertyRetriever;
024
025import com.nimbusds.common.servlet.ResourceRetriever;
026
027
028/**
029 * Example in-memory LDAP directory server for demonstration and testing 
030 * purposes. Access is limited to read and bind (authenticate) only.
031 *
032 * <p>The directory server is configured by a set of "exampleDirectoryServer.*"
033 * properties, see {@link Configuration}.
034 *
035 * <p>The example directory implements {@code ServletContextListener}. This 
036 * enables its automatic startup and shutdown in a servlet container (Java web 
037 * server), such as Apache Tomcat. When started from a servlet container the
038 * directory configuration is obtained from a properties file specified by a
039 * context parameter named {@code exampleDirectoryServer.configurationFile}.
040 */
041public class ExampleDirectory implements ServletContextListener {
042        
043        
044        /**
045         * The example directory server configuration.
046         */
047        public static class Configuration {
048        
049                
050                /**
051                 * If {@code true} the example directory server must be 
052                 * enabled.
053                 *
054                 * <p>Property key: exampleDirectoryServer.enable
055                 */
056                public final boolean enable;
057                
058                
059                /**
060                 * The default enable policy.
061                 */
062                public static final boolean DEFAULT_ENABLE = false;
063                
064                
065                /**
066                 * The port number on which the example directory server 
067                 * must accept LDAP client connections.
068                 *
069                 * <p>Property key: exampleDirectoryServer.port
070                 */
071                public final int port;
072                
073                
074                /**
075                 * The default port number.
076                 */
077                public static final int DEFAULT_PORT = 10389;
078                
079                
080                /**
081                 * Specifies an alternative schema for the example directory +
082                 * server, supplied in a single LDIF file. If {@code null} the 
083                 * default built-in server schema must be used.
084                 *
085                 * <p>Property key: exampleDirectoryServer.schema
086                 */
087                public final String schema;
088                
089                
090                /**
091                 * The base distinguished name (DN) of the directory information
092                 * tree.
093                 *
094                 * <p>Property key: exampleDirectoryServer.baseDN
095                 */
096                public final String baseDN;
097                
098                
099                /**
100                 * The initial directory information tree, supplied in a single
101                 * LDIF file. If {@code null} the directory will be left
102                 * empty.
103                 *
104                 * <p>Property key: exampleDirectoryServer.content
105                 */
106                public final String content;
107                
108                
109                /**
110                 * Creates a new example directory server configuration from the
111                 * specified properties.
112                 *
113                 * @param props The configuration properties. Must not be 
114                 *              {@code null}.
115                 *
116                 * @throws PropertyParseException On a missing or invalid 
117                 *                                property.
118                 */
119                public Configuration(final Properties props)
120                        throws PropertyParseException {
121                
122                        PropertyRetriever pr = new PropertyRetriever(props);
123                        
124                        enable = pr.getOptBoolean("exampleDirectoryServer.enable", DEFAULT_ENABLE);
125                        
126                        if (! enable) {
127                                
128                                port = DEFAULT_PORT;
129                                schema = null;
130                                baseDN = null;
131                                content = null;
132                                return;
133                        }
134                        
135                        // We're okay to read rest of config
136                        
137                        port = pr.getOptInt("exampleDirectoryServer.port", DEFAULT_PORT);
138                        
139                        String s = pr.getOptString("exampleDirectoryServer.schema", null);
140                        
141                        if (s == null || s.isEmpty())
142                                schema = null;
143                        else
144                                schema = s;
145                                
146                        baseDN = pr.getString("exampleDirectoryServer.baseDN");
147                        
148                        s = pr.getOptString("exampleDirectoryServer.content", null);
149                        
150                        if (s == null || s.isEmpty())
151                                content = null;
152                        else
153                                content = s;
154                }
155        }
156        
157        
158        /**
159         * The example in-memory directory server.
160         */
161        private InMemoryDirectoryServer ds = null;
162        
163        
164        /**
165         * The servlet context.
166         */
167        private ServletContext servletContext;
168        
169        
170        /** 
171         * The logger. 
172         */
173        private final Logger log = Logger.getLogger("MAIN");
174        
175        
176        /**
177         * If the specified filename is relative returns its full path, else the
178         * filename is left unchanged.
179         *
180         * <p>Uses the servlet context {@code getRealPath} method.
181         *
182         * @param filename The filename. Must not be {@code null}.
183         *
184         * @return The full path filename.
185         */
186        private String getFullPath(final String filename) {
187        
188                String fullPath = null;
189                
190                File file = new File(filename);
191                
192                if (file.isAbsolute())
193                        return filename;
194                else
195                        return servletContext.getRealPath(File.separator) + filename;
196        }
197        
198        
199        /**
200         * Starts the example in-memory directory server. 
201         *
202         * @param config The example directory server configuration. Must not
203         *               be {@code null}.
204         *
205         * @throws LDAPException If the in-memory directory server couldn't be
206         *                       started or its initialisation failed.
207         * @throws IOException   If a schema file was specified and it couldn't
208         *                       be read.
209         * @throws LDIFException If a schema file was specified that is not 
210         *                       valid LDIF.
211         */
212        public void start(final Configuration config)
213                throws LDAPException,
214                       IOException,
215                       LDIFException {
216                
217                if (! config.enable) {
218                
219                        log.info("Example directory server disabled");
220                        return;
221                }
222                
223                InMemoryListenerConfig listenerConfig = 
224                        InMemoryListenerConfig.createLDAPConfig("example-ds", config.port);
225
226                // Get alternative schema, if any
227
228                Schema schema = null;
229
230                if (config.schema != null) {
231
232                        String schemaFullPath = getFullPath(config.schema);
233                        schema = Schema.getSchema(schemaFullPath);
234                        
235                        log.info("Set example directory server schema to LDIF file " + schemaFullPath);
236                }
237
238
239                InMemoryDirectoryServerConfig dsConfig = new InMemoryDirectoryServerConfig(config.baseDN);
240
241                log.info("Set example directory server base DN to " + config.baseDN);
242
243                dsConfig.setSchema(schema);
244                dsConfig.setListenerConfigs(listenerConfig);
245
246                // Limit access to read and bind only
247                dsConfig.setAllowedOperationTypes(OperationType.BIND,
248                                                  OperationType.COMPARE,
249                                                  OperationType.SEARCH,
250                                                  OperationType.EXTENDED);
251
252                // Start server
253                ds = new InMemoryDirectoryServer(dsConfig);
254
255
256                // Populate directory with LDIF, if any
257
258                if (config.content != null) {
259
260                        String contentFullPath = getFullPath(config.content);
261                        ds.importFromLDIF(true, contentFullPath);
262
263                        log.info("Populated example directory server from LDIF file " + contentFullPath);
264                }
265
266                // Start listening on selected port
267                ds.startListening();
268
269                log.info("Started example directory server on port " + ds.getListenPort());
270        }
271        
272        
273        /**
274         * Stops the example in-memory directory server (if previously started).
275         * Information and status messages are logged at INFO level.
276         */
277        public void stop() {
278        
279                if (ds == null)
280                        return;
281                
282                // Clean all connections and stop server
283                ds.shutDown(true);
284                
285                log.info("Shut down example directory server");
286        }
287        
288        
289        /**
290         * Handler for servlet context startup events. Launches the example 
291         * in-memory directory server (if enabled per configuration). Exceptions
292         * are logged at ERROR level, information and status messages at INFO
293         * level.
294         *
295         * <p>The example directory server configuration is retrieved from a 
296         * properties file which location is specified by a servlet context
297         * parameter named {@code exampleDirectory.configurationFile}.
298         *
299         * @param sce A servlet context event.
300         */
301        public void contextInitialized(ServletContextEvent sce) {
302
303                servletContext = sce.getServletContext();
304                
305                // Read configuration
306                Configuration config = null;
307
308                try {
309                        Properties props = ResourceRetriever.getProperties(servletContext,
310                                                                           "exampleDirectory.configurationFile",
311                                                                           log);
312                
313                        config = new Configuration(props);
314
315                } catch (Exception e) {
316                
317                        log.error("Couldn't configure example directory server: " + e.getMessage());
318                        return;
319                }
320                
321                
322                // Start server
323                try {
324                        start(config);
325                        
326                } catch (LDAPException e) {
327                
328                        log.error("Couldn't start example directory server: " + e.getMessage());
329                        
330                } catch (IOException e) {
331
332                        log.error("Couldn't read schema file: " + e.getMessage());
333
334                } catch (LDIFException e) {
335
336                        log.error("Couldn't read schema file: " + e.getMessage());
337                }
338        }
339
340
341        /**
342         * Handler for servlet context shutdown events. Stops the example 
343         * in-memory directory server (if previously started).
344         *
345         * @param sce A servlet context event.
346         */
347        public void contextDestroyed(ServletContextEvent sce) {
348
349                stop();
350        }
351}
352