001    //$HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/branches/2.2_testing/src/org/deegree/tools/security/ActiveDirectoryImporter.java $
002    /*----------------    FILE HEADER  ------------------------------------------
003    
004     This file is part of deegree.
005     Copyright (C) 2001-2008 by:
006     EXSE, Department of Geography, University of Bonn
007     http://www.giub.uni-bonn.de/deegree/
008     lat/lon GmbH
009     http://www.lat-lon.de
010    
011     This library is free software; you can redistribute it and/or
012     modify it under the terms of the GNU Lesser General Public
013     License as published by the Free Software Foundation; either
014     version 2.1 of the License, or (at your option) any later version.
015    
016     This library is distributed in the hope that it will be useful,
017     but WITHOUT ANY WARRANTY; without even the implied warranty of
018     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
019     Lesser General Public License for more details.
020    
021     You should have received a copy of the GNU Lesser General Public
022     License along with this library; if not, write to the Free Software
023     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
024    
025     Contact:
026    
027     Andreas Poth
028     lat/lon GmbH
029     Aennchenstr. 19
030     53115 Bonn
031     Germany
032     E-Mail: poth@lat-lon.de
033    
034     Prof. Dr. Klaus Greve
035     Department of Geography
036     University of Bonn
037     Meckenheimer Allee 166
038     53115 Bonn
039     Germany
040     E-Mail: greve@giub.uni-bonn.de
041    
042     ---------------------------------------------------------------------------*/
043    package org.deegree.tools.security;
044    
045    import java.io.FileInputStream;
046    import java.io.IOException;
047    import java.io.PrintWriter;
048    import java.io.StringWriter;
049    import java.util.ArrayList;
050    import java.util.HashMap;
051    import java.util.Hashtable;
052    import java.util.Iterator;
053    import java.util.Properties;
054    
055    import javax.naming.Context;
056    import javax.naming.NamingEnumeration;
057    import javax.naming.NamingException;
058    import javax.naming.directory.Attribute;
059    import javax.naming.directory.Attributes;
060    import javax.naming.directory.SearchControls;
061    import javax.naming.directory.SearchResult;
062    import javax.naming.ldap.Control;
063    import javax.naming.ldap.InitialLdapContext;
064    import javax.naming.ldap.LdapContext;
065    import javax.naming.ldap.PagedResultsControl;
066    import javax.naming.ldap.PagedResultsResponseControl;
067    
068    import org.deegree.framework.mail.EMailMessage;
069    import org.deegree.framework.mail.MailHelper;
070    import org.deegree.framework.mail.SendMailException;
071    import org.deegree.security.GeneralSecurityException;
072    import org.deegree.security.UnauthorizedException;
073    import org.deegree.security.drm.ManagementException;
074    import org.deegree.security.drm.SecurityAccess;
075    import org.deegree.security.drm.SecurityAccessManager;
076    import org.deegree.security.drm.SecurityHelper;
077    import org.deegree.security.drm.SecurityTransaction;
078    import org.deegree.security.drm.UnknownException;
079    import org.deegree.security.drm.model.Group;
080    import org.deegree.security.drm.model.User;
081    
082    /**
083     * This class provides the functionality to synchronize the <code>User</code> and
084     * <code>Group</code> instances stored in a <code>SecurityManager</code> with an
085     * ActiveDirectory-Server.
086     * <p>
087     * Synchronization involves four steps:
088     * <ul>
089     * <li>synchronization of groups
090     * <li>synchronization of users
091     * <li>updating of the special group "SEC_ALL" (contains all users)
092     * <li>testing of subadmin-role validity (only one role per user max)
093     * </ul>
094     * Changes are committed after all steps succeeded. If an error occurs, changes in the
095     * <code>SecurityManager</code> are undone.
096     * <p>
097     * 
098     * 
099     * @version $Revision: 9346 $
100     * @author <a href="mailto:schneider@lat-lon.de">Markus Schneider</a>
101     * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a>
102     * @author last edited by: $Author: apoth $
103     * 
104     * @version $Revision: 9346 $, $Date: 2007-12-27 17:39:07 +0100 (Do, 27 Dez 2007) $
105     */
106    public class ActiveDirectoryImporter {
107    
108        private SecurityAccessManager manager;
109    
110        private SecurityAccess access;
111    
112        private SecurityTransaction trans;
113    
114        private User admin;
115    
116        private Hashtable<String, String> env;
117    
118        private LdapContext ctx;
119    
120        private Properties config;
121    
122        // mail configuration
123        private static String mailSender;
124    
125        private static String mailRcpt;
126    
127        private static String mailHost;
128    
129        private static boolean mailLog;
130    
131        // number of results to fetch in one batch
132        private int pageSize = 500;
133    
134        private StringBuffer logBuffer = new StringBuffer( 1000 );
135    
136        /**
137         * Constructs a new <code>ADExporter</code> -instance.
138         * <p>
139         * 
140         * @param config
141         * @throws NamingException
142         * @throws GeneralSecurityException
143         */
144        ActiveDirectoryImporter( Properties config ) throws NamingException, GeneralSecurityException {
145    
146            this.config = config;
147    
148            // retrieve mail configuration first
149            mailSender = getPropertySafe( "mailSender" );
150            mailRcpt = getPropertySafe( "mailRcpt" );
151            mailHost = getPropertySafe( "mailHost" );
152            mailLog = getPropertySafe( "mailLog" ).equals( "true" ) || getPropertySafe( "mailLog" ).equals( "yes" ) ? true
153                                                                                                                   : false;
154    
155            // get a SecurityManager (with an SQLRegistry)
156            Properties registryProperties = new Properties();
157            registryProperties.put( "driver", getPropertySafe( "sqlDriver" ) );
158            registryProperties.put( "url", getPropertySafe( "sqlLogon" ) );
159            registryProperties.put( "user", getPropertySafe( "sqlUser" ) );
160            registryProperties.put( "password", getPropertySafe( "sqlPass" ) );
161    
162            // default timeout: 20 min
163            long timeout = 1200000;
164            try {
165                timeout = Long.parseLong( getPropertySafe( "timeout" ) );
166            } catch ( NumberFormatException e ) {
167                logBuffer.append( "Specified property value for timeout invalid. " + "Defaulting to 1200 (secs)." );
168            }
169    
170            if ( !SecurityAccessManager.isInitialized() ) {
171                SecurityAccessManager.initialize( "org.deegree.security.drm.SQLRegistry", registryProperties, timeout );
172            }
173    
174            manager = SecurityAccessManager.getInstance();
175            admin = manager.getUserByName( getPropertySafe( "u3rAdminName" ) );
176            admin.authenticate( getPropertySafe( "u3rAdminPassword" ) );
177    
178            // prepare LDAP connection
179            String jndiURL = "ldap://" + getPropertySafe( "ldapHost" ) + ":389/";
180            String initialContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
181            String authenticationMode = "simple";
182            String contextReferral = "ignore";
183            env = new Hashtable<String, String>();
184            env.put( Context.INITIAL_CONTEXT_FACTORY, initialContextFactory );
185            env.put( Context.PROVIDER_URL, jndiURL );
186            env.put( Context.SECURITY_AUTHENTICATION, authenticationMode );
187            env.put( Context.SECURITY_PRINCIPAL, getPropertySafe( "ldapUser" ) );
188            env.put( Context.SECURITY_CREDENTIALS, getPropertySafe( "ldapPass" ) );
189            env.put( Context.REFERRAL, contextReferral );
190    
191            access = manager.acquireAccess( admin );
192            trans = manager.acquireTransaction( admin );
193            ctx = new InitialLdapContext( env, null );
194    
195        }
196    
197        /**
198         * Returns a configuration property. If it is not defined, an exception is thrown.
199         * <p>
200         * 
201         * @param name
202         * @return a configuration property. If it is not defined, an exception is thrown.
203         */
204        private String getPropertySafe( String name ) {
205    
206            String value = config.getProperty( name );
207            if ( value == null ) {
208                throw new RuntimeException( "Configuration does not define needed property '" + name + "'." );
209            }
210            return value;
211        }
212    
213        /**
214         * Synchronizes the AD's group objects with the SecurityManager's group objects.
215         * <p>
216         * 
217         * @return
218         * @throws NamingException
219         * @throws IOException
220         * @throws GeneralSecurityException
221         * @throws UnauthorizedException
222         */
223        HashMap synchronizeGroups()
224                                throws NamingException, IOException, UnauthorizedException, GeneralSecurityException {
225            // keys are names (Strings), values are Group-objects
226            HashMap<String,Group> groupMap = new HashMap<String,Group>( 20 );
227            // keys are distinguishedNames (Strings), values are Group-objects
228            HashMap<String,Group> groupMap2 = new HashMap<String,Group>( 20 );
229            // keys are names (Strings), values are NamingEnumeration-objects
230            HashMap<String,NamingEnumeration> memberOfMap = new HashMap<String,NamingEnumeration>( 20 );
231    
232            byte[] cookie = null;
233    
234            // specify the ids of the attributes to return
235            String[] attrIDs = { "distinguishedName", getPropertySafe( "groupName" ), getPropertySafe( "groupTitle" ),
236                                getPropertySafe( "groupMemberOf" ) };
237    
238            // set SearchControls
239            SearchControls ctls = new SearchControls();
240            ctls.setReturningAttributes( attrIDs );
241            ctls.setSearchScope( SearchControls.SUBTREE_SCOPE );
242    
243            // specify the search filter to match
244            // ask for objects that have the attribute "objectCategory" =
245            // "CN=Group*"
246            String filter = getPropertySafe( "groupFilter" );
247            String context = getPropertySafe( "groupContext" );
248    
249            // create initial PagedResultsControl
250            ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, false ) } );
251    
252            // phase 1: make sure that all groups from AD are present in the
253            // SecurityManager
254            // (register them to the SecurityManager if necessary)
255            do {
256                // perform the search
257                NamingEnumeration answer = ctx.search( context, filter, ctls );
258    
259                // process results of the last batch
260                while ( answer.hasMoreElements() ) {
261                    SearchResult result = (SearchResult) answer.nextElement();
262                    Attributes atts = result.getAttributes();
263                    String distinguishedName = (String) atts.get( "distinguishedName" ).get();
264                    String name = (String) atts.get( getPropertySafe( "groupName" ) ).get();
265                    String title = (String) atts.get( getPropertySafe( "groupTitle" ) ).get();
266    
267                    // check if group is already registered
268                    Group group = null;
269                    try {
270                        group = access.getGroupByName( name );
271                    } catch ( UnknownException e ) {
272                        // no -> register group
273                        logBuffer.append( "Registering group: " + name + "\n" );
274                        group = trans.registerGroup( name, title );
275                    }
276                    groupMap.put( name, group );
277                    groupMap2.put( distinguishedName, group );
278                    if ( atts.get( getPropertySafe( "groupMemberOf" ) ) != null ) {
279                        memberOfMap.put( name, atts.get( getPropertySafe( "groupMemberOf" ) ).getAll() );
280                    }
281                }
282    
283                // examine the paged results control response
284                Control[] controls = ctx.getResponseControls();
285                if ( controls != null ) {
286                    for ( int i = 0; i < controls.length; i++ ) {
287                        if ( controls[i] instanceof PagedResultsResponseControl ) {
288                            PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
289                            // total = prrc.getResultSize();
290                            cookie = prrc.getCookie();
291                        }
292                    }
293                }
294    
295                if ( cookie != null ) {
296                    // re-activate paged results
297                    ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, cookie, Control.CRITICAL ) } );
298                }
299            } while ( cookie != null );
300    
301            // phase 2: make sure that all groups from the SecurityManager are known
302            // to the AD
303            // (deregister them from the SecurityManager if necessary)
304            Group[] sMGroups = access.getAllGroups();
305            for ( int i = 0; i < sMGroups.length; i++ ) {
306                if ( groupMap.get( sMGroups[i].getName() ) == null && sMGroups[i].getID() != Group.ID_SEC_ADMIN
307                     && !( sMGroups[i].getName().equals( "SEC_ALL" ) ) ) {
308                    logBuffer.append( "Deregistering group: " + sMGroups[i].getName() + "\n" );
309                    trans.deregisterGroup( sMGroups[i] );
310                }
311            }
312    
313            // phase 3: set the membership-relations between the groups
314            Iterator it = groupMap.keySet().iterator();
315            while ( it.hasNext() ) {
316                String name = (String) it.next();
317                Group group = groupMap.get( name );
318                NamingEnumeration memberOf = memberOfMap.get( name );
319                ArrayList<Group> memberOfList = new ArrayList<Group>( 5 );
320    
321                if ( memberOf != null ) {
322                    while ( memberOf.hasMoreElements() ) {
323                        String memberGroupName = (String) memberOf.nextElement();
324                        Group memberGroup = groupMap2.get( memberGroupName );
325                        if ( memberGroup != null ) {
326                            memberOfList.add( memberGroup );
327                        } else {
328                            logBuffer.append( "Group " + name + " is member of unknown group " + memberGroupName
329                                              + ". Membership ignored.\n" );
330                        }
331                    }
332                }
333                Group[] newGroups = memberOfList.toArray( new Group[memberOfList.size()] );
334                trans.setGroupsForGroup( group, newGroups );
335            }
336            return groupMap2;
337        }
338    
339        /**
340         * Synchronizes the AD's user objects with the SecurityManager's user objects.
341         * <p>
342         * 
343         * @param groups
344         * 
345         * @throws NamingException
346         * @throws IOException
347         * @throws GeneralSecurityException
348         * @throws UnauthorizedException
349         * 
350         */
351        void synchronizeUsers( HashMap groups )
352                                throws NamingException, IOException, UnauthorizedException, GeneralSecurityException {
353            // keys are names (Strings), values are User-objects
354            HashMap<String,User> userMap = new HashMap<String,User>( 20 );
355            // keys are names (Strings), values are NamingEnumeration-objects
356            HashMap<String,NamingEnumeration> memberOfMap = new HashMap<String,NamingEnumeration>( 20 );
357    
358            byte[] cookie = null;
359    
360            // specify the ids of the attributes to return
361            String[] attrIDs = { getPropertySafe( "userName" ), getPropertySafe( "userTitle" ),
362                                getPropertySafe( "userFirstName" ), getPropertySafe( "userLastName" ),
363                                getPropertySafe( "userMail" ), getPropertySafe( "userMemberOf" ) };
364    
365            SearchControls ctls = new SearchControls();
366            ctls.setReturningAttributes( attrIDs );
367            ctls.setSearchScope( SearchControls.SUBTREE_SCOPE );
368    
369            // specify the search filter to match
370            String filter = getPropertySafe( "userFilter" );
371            String context = getPropertySafe( "userContext" );
372    
373            // create initial PagedResultsControl
374            ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, false ) } );
375    
376            // phase 1: make sure that all users from AD are present in the
377            // SecurityManager
378            // (register them to the SecurityManager if necessary)
379            do {
380                // perform the search
381                NamingEnumeration answer = ctx.search( context, filter, ctls );
382    
383                // process results of the last batch
384                while ( answer.hasMoreElements() ) {
385                    SearchResult result = (SearchResult) answer.nextElement();
386    
387                    Attributes atts = result.getAttributes();
388                    Attribute nameAtt = atts.get( getPropertySafe( "userName" ) );
389                    // Attribute titleAtt = atts.get( getPropertySafe( "userTitle" ) );
390                    Attribute firstNameAtt = atts.get( getPropertySafe( "userFirstName" ) );
391                    Attribute lastNameAtt = atts.get( getPropertySafe( "userLastName" ) );
392                    Attribute mailAtt = atts.get( getPropertySafe( "userMail" ) );
393                    Attribute memberOfAtt = atts.get( getPropertySafe( "userMemberOf" ) );
394    
395                    String name = (String) nameAtt.get();
396                    // String title = titleAtt != null ? (String) titleAtt.get() : "";
397                    String firstName = firstNameAtt != null ? (String) firstNameAtt.get() : "" + "";
398                    String lastName = lastNameAtt != null ? (String) lastNameAtt.get() : "" + "";
399                    String mail = mailAtt != null ? (String) mailAtt.get() : "";
400    
401                    // check if user is already registered
402                    User user = null;
403                    try {
404                        user = access.getUserByName( name );
405                    } catch ( UnknownException e ) {
406                        // no -> register user
407                        logBuffer.append( "Registering user: " + name + "\n" );
408                        user = trans.registerUser( name, null, lastName, firstName, mail );
409                    }
410                    userMap.put( name, user );
411    
412                    if ( memberOfAtt != null ) {
413                        memberOfMap.put( name, memberOfAtt.getAll() );
414                    }
415                }
416    
417                // examine the paged results control response
418                Control[] controls = ctx.getResponseControls();
419                if ( controls != null ) {
420                    for ( int i = 0; i < controls.length; i++ ) {
421                        if ( controls[i] instanceof PagedResultsResponseControl ) {
422                            PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
423                            // total = prrc.getResultSize();
424                            cookie = prrc.getCookie();
425                        }
426                    }
427                }
428    
429                if ( cookie != null ) {
430                    // re-activate paged results
431                    ctx.setRequestControls( new Control[] { new PagedResultsControl( pageSize, cookie, Control.CRITICAL ) } );
432                }
433            } while ( cookie != null );
434    
435            // phase 2: make sure that all users from the SecurityManager are known
436            // to the AD
437            // (deregister them from the SecurityManager if necessary)
438            User[] sMUsers = access.getAllUsers();
439            for ( int i = 0; i < sMUsers.length; i++ ) {
440                if ( userMap.get( sMUsers[i].getName() ) == null && sMUsers[i].getID() != User.ID_SEC_ADMIN ) {
441                    logBuffer.append( "Deregistering user: " + sMUsers[i].getName() + "\n" );
442                    trans.deregisterUser( sMUsers[i] );
443                }
444            }
445    
446            // phase 3: set the membership-relations between the groups and the
447            // users
448            Iterator it = userMap.keySet().iterator();
449            while ( it.hasNext() ) {
450                String name = (String) it.next();
451                User user = userMap.get( name );
452                NamingEnumeration memberOf = memberOfMap.get( name );
453                ArrayList<Group> memberOfList = new ArrayList<Group>( 5 );
454                if ( memberOf != null ) {
455                    while ( memberOf.hasMoreElements() ) {
456                        String memberGroupName = (String) memberOf.nextElement();
457                        Group memberGroup = (Group) groups.get( memberGroupName );
458                        if ( memberGroup != null ) {
459                            memberOfList.add( memberGroup );
460                        } else {
461                            logBuffer.append( "User " + name + " is member of unknown group " + memberGroupName
462                                              + ". Membership ignored.\n" );
463                        }
464                    }
465                }
466                Group[] newGroups = memberOfList.toArray( new Group[memberOfList.size()] );
467                trans.setGroupsForUser( user, newGroups );
468            }
469    
470        }
471    
472        /**
473         * Updates the special group "SEC_ALL" (contains all users).
474         * <p>
475         * 
476         * @throws GeneralSecurityException
477         * 
478         */
479        void updateSecAll()
480                                throws GeneralSecurityException {
481            Group secAll = null;
482    
483            // phase1: make sure that group "SEC_ALL" exists
484            // (register it if necessary)
485            try {
486                secAll = access.getGroupByName( "SEC_ALL" );
487            } catch ( UnknownException e ) {
488                secAll = trans.registerGroup( "SEC_ALL", "SEC_ALL" );
489            }
490    
491            // phase2: set all users to be members of this group
492            User[] allUsers = access.getAllUsers();
493            trans.setUsersInGroup( secAll, allUsers );
494    
495        }
496    
497        /**
498         * Checks subadmin-role validity (each user one role max).
499         * <p>
500         * 
501         * @throws ManagementException
502         * @throws GeneralSecurityException
503         */
504        void checkSubadminRoleValidity()
505                                throws ManagementException, GeneralSecurityException {
506            SecurityHelper.checkSubadminRoleValidity( access );
507        }
508    
509        /**
510         * Aborts the synchronization process and undoes all changes.
511         */
512        public void abortChanges() {
513            if ( manager != null && trans != null ) {
514                try {
515                    manager.abortTransaction( trans );
516                } catch ( GeneralSecurityException e ) {
517                    e.printStackTrace();
518                }
519            }
520            if ( ctx != null ) {
521                try {
522                    ctx.close();
523                } catch ( NamingException e ) {
524                    e.printStackTrace();
525                }
526            }
527        }
528    
529        /**
530         * Ends the synchronization process and commits all changes.
531         */
532        public void commitChanges() {
533            if ( manager != null && trans != null ) {
534                try {
535                    manager.commitTransaction( trans );
536                } catch ( GeneralSecurityException e ) {
537                    e.printStackTrace();
538                }
539            }
540            if ( ctx != null ) {
541                try {
542                    ctx.close();
543                } catch ( NamingException e ) {
544                    e.printStackTrace();
545                }
546            }
547        }
548    
549        /**
550         * Sends an eMail to inform the admin that something went wrong.
551         * <p>
552         * NOTE: This is static, because it must be usable even when the construction of the ADExporter
553         * failed.
554         * <p>
555         * 
556         * @param e
557         */
558        public static void sendError( Exception e ) {
559    
560            try {
561                String mailMessage = "Beim Synchronisieren des ActiveDirectory mit der HUIS-"
562                                     + "Sicherheitsdatenbank ist ein Fehler aufgetreten.\n"
563                                     + "Die Synchronisierung wurde NICHT durchgeführt, der letzte "
564                                     + "Stand wurde wiederhergestellt.\n";
565                StringWriter sw = new StringWriter();
566                PrintWriter writer = new PrintWriter( sw );
567                e.printStackTrace( writer );
568                mailMessage += "\n\nDie Java-Fehlermeldung lautet:\n" + sw.getBuffer();
569    
570                mailMessage += "\n\nMit freundlichem Gruss,\nIhr ADExporter";
571                MailHelper.createAndSendMail(
572                                              new EMailMessage( mailSender, mailRcpt, "Fehler im ADExporter", mailMessage ),
573                                              mailHost );
574            } catch ( SendMailException ex ) {
575                ex.printStackTrace();
576            }
577    
578        }
579    
580        /**
581         * Sends an eMail with a log of the transaction.
582         * <p>
583         */
584        public void sendLog() {
585    
586            try {
587                String mailMessage = "Die Synchronisierung der HUIS-Sicherheitsdatenbank mit "
588                                     + "dem ActiveDirectory wurde erfolgreich durchgeführt:\n\n";
589                if ( logBuffer.length() == 0 ) {
590                    mailMessage += "Keine Änderungen.";
591                } else {
592                    mailMessage += logBuffer.toString();
593                }
594                mailMessage += "\n\nMit freundlichem Gruss,\nIhr ADExporter";
595                EMailMessage emm = new EMailMessage( mailSender, mailRcpt, "ActiveDirectory Sychronisierung durchgeführt",
596                                                     mailMessage );
597                MailHelper.createAndSendMail( emm, mailHost );
598            } catch ( SendMailException ex ) {
599                ex.printStackTrace();
600            }
601    
602        }
603    
604        /**
605         * @param args
606         * @throws Exception
607         */
608        public static void main( String[] args )
609                                throws Exception {
610    
611            if ( args.length != 1 ) {
612                System.out.println( "USAGE: ADExporter configfile" );
613                System.exit( 0 );
614            }
615    
616            long begin = System.currentTimeMillis();
617            System.out.println( "Beginning synchronisation..." );
618    
619            ActiveDirectoryImporter exporter = null;
620            try {
621                Properties config = new Properties();
622                config.load( new FileInputStream( args[0] ) );
623                exporter = new ActiveDirectoryImporter( config );
624    
625                HashMap groups = exporter.synchronizeGroups();
626                exporter.synchronizeUsers( groups );
627                exporter.updateSecAll();
628                exporter.checkSubadminRoleValidity();
629                exporter.commitChanges();
630            } catch ( Exception e ) {
631                if ( exporter != null ) {
632                    exporter.abortChanges();
633                }
634                sendError( e );
635                System.err.println( "Synchronisation has been aborted. Error message: " );
636                e.printStackTrace();
637                System.exit( 0 );
638            }
639    
640            if ( mailLog ) {
641                exporter.sendLog();
642            }
643    
644            System.out.println( "Synchronisation took " + ( System.currentTimeMillis() - begin ) + " milliseconds." );
645    
646            System.exit( 0 );
647        }
648    }