001    /* 
002        Licensed to the Apache Software Foundation (ASF) under one
003        or more contributor license agreements.  See the NOTICE file
004        distributed with this work for additional information
005        regarding copyright ownership.  The ASF licenses this file
006        to you under the Apache License, Version 2.0 (the
007        "License"); you may not use this file except in compliance
008        with the License.  You may obtain a copy of the License at
009    
010           http://www.apache.org/licenses/LICENSE-2.0
011    
012        Unless required by applicable law or agreed to in writing,
013        software distributed under the License is distributed on an
014        "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015        KIND, either express or implied.  See the License for the
016        specific language governing permissions and limitations
017        under the License.  
018     */
019    package org.apache.wiki.auth.user;
020    
021    import java.io.BufferedWriter;
022    import java.io.File;
023    import java.io.FileNotFoundException;
024    import java.io.FileOutputStream;
025    import java.io.IOException;
026    import java.io.OutputStreamWriter;
027    import java.io.Serializable;
028    import java.security.Principal;
029    import java.text.DateFormat;
030    import java.text.ParseException;
031    import java.text.SimpleDateFormat;
032    import java.util.Date;
033    import java.util.Map;
034    import java.util.Properties;
035    import java.util.SortedSet;
036    import java.util.TreeSet;
037    
038    import javax.xml.parsers.DocumentBuilderFactory;
039    import javax.xml.parsers.ParserConfigurationException;
040    
041    import org.apache.wiki.WikiEngine;
042    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
043    import org.apache.wiki.auth.NoSuchPrincipalException;
044    import org.apache.wiki.auth.WikiPrincipal;
045    import org.apache.wiki.auth.WikiSecurityException;
046    import org.apache.wiki.util.Serializer;
047    import org.w3c.dom.Document;
048    import org.w3c.dom.Element;
049    import org.w3c.dom.Node;
050    import org.w3c.dom.NodeList;
051    import org.w3c.dom.Text;
052    import org.xml.sax.SAXException;
053    
054    /**
055     * <p>Manages {@link DefaultUserProfile} objects using XML files for persistence.
056     * Passwords are hashed using SHA1. User entries are simple <code>&lt;user&gt;</code>
057     * elements under the root. User profile properties are attributes of the
058     * element. For example:</p>
059     * <blockquote><code>
060     * &lt;users&gt;<br/>
061     * &nbsp;&nbsp;&lt;user loginName="janne" fullName="Janne Jalkanen"<br/> 
062     * &nbsp;&nbsp;&nbsp;&nbsp;wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/>
063     * &nbsp;&nbsp;&nbsp;&nbsp;password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/&gt;<br/>
064     * &lt;/users&gt;
065     * </code></blockquote> 
066     * <p>In this example, the un-hashed password is <code>myP@5sw0rd</code>. Passwords are hashed without salt.</p>
067     * @since 2.3
068     */
069    
070    // FIXME: If the DB is shared across multiple systems, it's possible to lose accounts
071    //        if two people add new accounts right after each other from different wikis.
072    public class XMLUserDatabase extends AbstractUserDatabase {
073    
074        /**
075         * The jspwiki.properties property specifying the file system location of
076         * the user database.
077         */
078        public static final String  PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile";
079        
080        private static final String DEFAULT_USERDATABASE = "userdatabase.xml";
081    
082        private static final String ATTRIBUTES_TAG    = "attributes";
083        
084        private static final String CREATED           = "created";
085        
086        private static final String EMAIL             = "email";
087    
088        private static final String FULL_NAME         = "fullName";
089    
090        private static final String LOGIN_NAME        = "loginName";
091    
092        private static final String LAST_MODIFIED     = "lastModified";
093        
094        private static final String LOCK_EXPIRY       = "lockExpiry";
095    
096        private static final String PASSWORD          = "password";
097    
098        private static final String UID               = "uid";
099    
100        private static final String USER_TAG          = "user";
101    
102        private static final String WIKI_NAME         = "wikiName";
103    
104        private static final String DATE_FORMAT       = "yyyy.MM.dd 'at' HH:mm:ss:SSS z";
105    
106        private Document            c_dom             = null;
107    
108        private File                c_file            = null;
109    
110        /**
111         * Looks up and deletes the first {@link UserProfile} in the user database
112         * that matches a profile having a given login name. If the user database
113         * does not contain a user with a matching attribute, throws a
114         * {@link NoSuchPrincipalException}.
115         * @param loginName the login name of the user profile that shall be deleted
116         */
117        public synchronized void deleteByLoginName( String loginName ) throws NoSuchPrincipalException, WikiSecurityException
118        {
119            if ( c_dom == null )
120            {
121                throw new WikiSecurityException( "FATAL: database does not exist" );
122            }
123                
124            NodeList users = c_dom.getDocumentElement().getElementsByTagName( USER_TAG );
125            for( int i = 0; i < users.getLength(); i++ )
126            {
127                Element user = (Element) users.item( i );
128                if ( user.getAttribute( LOGIN_NAME ).equals( loginName ) )
129                {
130                    c_dom.getDocumentElement().removeChild(user);
131                    
132                    // Commit to disk
133                    saveDOM();
134                    return;
135                }
136            }
137            throw new NoSuchPrincipalException( "Not in database: " + loginName );
138        }        
139    
140        /**
141         * Looks up and returns the first {@link UserProfile}in the user database
142         * that matches a profile having a given e-mail address. If the user
143         * database does not contain a user with a matching attribute, throws a
144         * {@link NoSuchPrincipalException}.
145         * @param index the e-mail address of the desired user profile
146         * @return the user profile
147         * @see org.apache.wiki.auth.user.UserDatabase#findByEmail(String)
148         */
149        public UserProfile findByEmail( String index ) throws NoSuchPrincipalException
150        {
151            UserProfile profile = findByAttribute( EMAIL, index );
152            if ( profile != null )
153            {
154                return profile;
155            }
156            throw new NoSuchPrincipalException( "Not in database: " + index );
157        }
158    
159        /**
160         * Looks up and returns the first {@link UserProfile}in the user database
161         * that matches a profile having a given full name. If the user database
162         * does not contain a user with a matching attribute, throws a
163         * {@link NoSuchPrincipalException}.
164         * @param index the fill name of the desired user profile
165         * @return the user profile
166         * @see org.apache.wiki.auth.user.UserDatabase#findByFullName(java.lang.String)
167         */
168        public UserProfile findByFullName( String index ) throws NoSuchPrincipalException
169        {
170            UserProfile profile = findByAttribute( FULL_NAME, index );
171            if ( profile != null )
172            {
173                return profile;
174            }
175            throw new NoSuchPrincipalException( "Not in database: " + index );
176        }
177    
178        /**
179         * Looks up and returns the first {@link UserProfile}in the user database
180         * that matches a profile having a given login name. If the user database
181         * does not contain a user with a matching attribute, throws a
182         * {@link NoSuchPrincipalException}.
183         * @param index the login name of the desired user profile
184         * @return the user profile
185         * @see org.apache.wiki.auth.user.UserDatabase#findByLoginName(java.lang.String)
186         */
187        public UserProfile findByLoginName( String index ) throws NoSuchPrincipalException
188        {
189            UserProfile profile = findByAttribute( LOGIN_NAME, index );
190            if ( profile != null )
191            {
192                return profile;
193            }
194            throw new NoSuchPrincipalException( "Not in database: " + index );
195        }
196    
197        /**
198         * {@inheritDoc}
199         */
200        public UserProfile findByUid( String uid ) throws NoSuchPrincipalException
201        {
202            UserProfile profile = findByAttribute( UID, uid );
203            if ( profile != null )
204            {
205                return profile;
206            }
207            throw new NoSuchPrincipalException( "Not in database: " + uid );
208        }
209    
210        /**
211         * Looks up and returns the first {@link UserProfile}in the user database
212         * that matches a profile having a given wiki name. If the user database
213         * does not contain a user with a matching attribute, throws a
214         * {@link NoSuchPrincipalException}.
215         * @param index the wiki name of the desired user profile
216         * @return the user profile
217         * @see org.apache.wiki.auth.user.UserDatabase#findByWikiName(java.lang.String)
218         */
219        public UserProfile findByWikiName( String index ) throws NoSuchPrincipalException
220        {
221            UserProfile profile = findByAttribute( WIKI_NAME, index );
222            if ( profile != null )
223            {
224                return profile;
225            }
226            throw new NoSuchPrincipalException( "Not in database: " + index );
227        }
228    
229        /**
230         * Returns all WikiNames that are stored in the UserDatabase
231         * as an array of WikiPrincipal objects. If the database does not
232         * contain any profiles, this method will return a zero-length
233         * array.
234         * @return the WikiNames
235         * @throws WikiSecurityException In case things fail.
236         */
237        public Principal[] getWikiNames() throws WikiSecurityException
238        {
239            if ( c_dom == null )
240            {
241                throw new IllegalStateException( "FATAL: database does not exist" );
242            }
243            SortedSet<Principal> principals = new TreeSet<Principal>();
244            NodeList users = c_dom.getElementsByTagName( USER_TAG );
245            for( int i = 0; i < users.getLength(); i++ )
246            {
247                Element user = (Element) users.item( i );
248                String wikiName = user.getAttribute( WIKI_NAME );
249                if ( wikiName == null )
250                {
251                    log.warn( "Detected null wiki name in XMLUserDataBase. Check your user database." );
252                }
253                else
254                {
255                    Principal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME );
256                    principals.add( principal );
257                }
258            }
259            return principals.toArray( new Principal[principals.size()] );
260        }
261        
262        /**
263         * Initializes the user database based on values from a Properties object.
264         * The properties object must contain a file path to the XML database file
265         * whose key is {@link #PROP_USERDATABASE}.
266         * @see org.apache.wiki.auth.user.UserDatabase#initialize(org.apache.wiki.WikiEngine,
267         *      java.util.Properties)
268         * @throws NoRequiredPropertyException if the user database cannot be located, parsed, or opened
269         */
270        public void initialize( WikiEngine engine, Properties props ) throws NoRequiredPropertyException
271        {
272            File defaultFile = null;
273            if( engine.getRootPath() == null )
274            {
275                log.warn( "Cannot identify JSPWiki root path"  );
276                defaultFile = new File( "WEB-INF/" + DEFAULT_USERDATABASE ).getAbsoluteFile();
277            }
278            else
279            {
280                defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_USERDATABASE );
281            }
282    
283            // Get database file location
284            String file = props.getProperty( PROP_USERDATABASE );
285            if( file == null )
286            {
287                log.warn( "XML user database property " + PROP_USERDATABASE + " not found; trying " + defaultFile  );
288                c_file = defaultFile;
289            }
290            else 
291            {
292                c_file = new File( file );
293            }
294    
295            log.info("XML user database at "+c_file.getAbsolutePath());
296            
297            buildDOM();
298            sanitizeDOM();
299        }
300        
301        private void buildDOM()
302        {
303            // Read DOM
304            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
305            factory.setValidating( false );
306            factory.setExpandEntityReferences( false );
307            factory.setIgnoringComments( true );
308            factory.setNamespaceAware( false );
309            try
310            {
311                c_dom = factory.newDocumentBuilder().parse( c_file );
312                log.debug( "Database successfully initialized" );
313                c_lastModified = c_file.lastModified();
314                c_lastCheck    = System.currentTimeMillis();
315            }
316            catch( ParserConfigurationException e )
317            {
318                log.error( "Configuration error: " + e.getMessage() );
319            }
320            catch( SAXException e )
321            {
322                log.error( "SAX error: " + e.getMessage() );
323            }
324            catch( FileNotFoundException e )
325            {
326                log.info("User database not found; creating from scratch...");
327            }
328            catch( IOException e )
329            {
330                log.error( "IO error: " + e.getMessage() );
331            }
332            if ( c_dom == null )
333            {
334                try
335                {
336                    //
337                    //  Create the DOM from scratch
338                    //
339                    c_dom = factory.newDocumentBuilder().newDocument();
340                    c_dom.appendChild( c_dom.createElement( "users") );
341                }
342                catch( ParserConfigurationException e )
343                {
344                    log.fatal( "Could not create in-memory DOM" );
345                }
346            }
347        }
348        
349        private void saveDOM() throws WikiSecurityException
350        {
351            if ( c_dom == null )
352            {
353                log.fatal( "User database doesn't exist in memory." );
354            }
355    
356            File newFile = new File( c_file.getAbsolutePath() + ".new" );
357            try
358            {
359                BufferedWriter io = new BufferedWriter( new OutputStreamWriter ( 
360                        new FileOutputStream( newFile ), "UTF-8" ) );
361                
362                // Write the file header and document root
363                io.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
364                io.write("<users>\n");
365                
366                // Write each profile as a <user> node
367                Element root = c_dom.getDocumentElement();
368                NodeList nodes = root.getElementsByTagName( USER_TAG );  
369                for( int i = 0; i < nodes.getLength(); i++ )
370                {
371                    Element user = (Element)nodes.item( i );
372                    io.write( "    <" + USER_TAG + " ");
373                    io.write( UID );
374                    io.write( "=\"" + user.getAttribute( UID ) + "\" " );
375                    io.write( LOGIN_NAME );
376                    io.write( "=\"" + user.getAttribute( LOGIN_NAME ) + "\" " );
377                    io.write( WIKI_NAME );
378                    io.write( "=\"" + user.getAttribute( WIKI_NAME ) + "\" " );
379                    io.write( FULL_NAME );
380                    io.write( "=\"" + user.getAttribute( FULL_NAME ) + "\" " );
381                    io.write( EMAIL );
382                    io.write( "=\"" + user.getAttribute( EMAIL ) + "\" " );
383                    io.write( PASSWORD );
384                    io.write( "=\"" + user.getAttribute( PASSWORD ) + "\" " );
385                    io.write( CREATED );
386                    io.write( "=\"" + user.getAttribute( CREATED ) + "\" " );
387                    io.write( LAST_MODIFIED );
388                    io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " );
389                    io.write( LOCK_EXPIRY );
390                    io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " );
391                    io.write( ">" );
392                    NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
393                    for ( int j = 0; j < attributes.getLength(); j++ )
394                    {
395                        Element attribute = (Element)attributes.item( j );
396                        String value = extractText( attribute );
397                        io.write( "\n        <" + ATTRIBUTES_TAG + ">" );
398                        io.write( value );
399                        io.write( "</" + ATTRIBUTES_TAG + ">" );
400                    }
401                    io.write("\n    </" +USER_TAG + ">\n");
402                }
403                io.write("</users>");
404                io.close();
405            }
406            catch ( IOException e )
407            {
408                throw new WikiSecurityException( e.getLocalizedMessage(), e );
409            }
410    
411            // Copy new file over old version
412            File backup = new File( c_file.getAbsolutePath() + ".old" );
413            if ( backup.exists() )
414            {
415                if ( !backup.delete() )
416                {
417                    log.error( "Could not delete old user database backup: " + backup );
418                }
419            }
420            if ( !c_file.renameTo( backup ) )
421            {
422                log.error( "Could not create user database backup: " + backup );
423            }
424            if ( !newFile.renameTo( c_file ) )
425            {
426                log.error( "Could not save database: " + backup + " restoring backup." );
427                if ( !backup.renameTo( c_file ) )
428                {
429                    log.error( "Restore failed. Check the file permissions." );
430                }
431                log.error( "Could not save database: " + c_file + ". Check the file permissions" );
432            }
433        }
434        
435        private long c_lastCheck    = 0;
436        private long c_lastModified = 0;
437        
438        private void checkForRefresh()
439        {
440            long time = System.currentTimeMillis();
441            
442            if( time - c_lastCheck > 60*1000L )
443            {
444                long lastModified = c_file.lastModified();
445                
446                if( lastModified > c_lastModified )
447                {
448                    buildDOM();
449                }
450            }
451        }
452    
453        /**
454         * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String)
455         */
456        public synchronized void rename(String loginName, String newName) throws NoSuchPrincipalException, DuplicateUserException, WikiSecurityException
457        {
458            if ( c_dom == null )
459            {
460                log.fatal( "Could not rename profile '" + loginName + "'; database does not exist" );
461                throw new IllegalStateException( "FATAL: database does not exist" );
462            }
463            checkForRefresh();
464            
465            // Get the existing user; if not found, throws NoSuchPrincipalException
466            UserProfile profile = findByLoginName( loginName );
467            
468            // Get user with the proposed name; if found, it's a collision
469            try 
470            {
471                UserProfile otherProfile = findByLoginName( newName );
472                if ( otherProfile != null )
473                {
474                    throw new DuplicateUserException( "security.error.cannot.rename", newName );
475                }
476            }
477            catch ( NoSuchPrincipalException e )
478            {
479                // Good! That means it's safe to save using the new name
480            }
481            
482            // Find the user with the old login id attribute, and change it
483            NodeList users = c_dom.getElementsByTagName( USER_TAG );
484            for( int i = 0; i < users.getLength(); i++ )
485            {
486                Element user = (Element) users.item( i );
487                if ( user.getAttribute( LOGIN_NAME ).equals( loginName ) )
488                {
489                    DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
490                    Date modDate = new Date( System.currentTimeMillis() );
491                    setAttribute( user, LOGIN_NAME, newName );
492                    setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) );
493                    profile.setLoginName( newName );
494                    profile.setLastModified( modDate );
495                    break;
496                }
497            }
498            
499            // Commit to disk
500            saveDOM();
501        }
502        
503        /**
504         * Saves a {@link UserProfile}to the user database, overwriting the
505         * existing profile if it exists. The user name under which the profile
506         * should be saved is returned by the supplied profile's
507         * {@link UserProfile#getLoginName()}method.
508         * @param profile the user profile to save
509         * @throws WikiSecurityException if the profile cannot be saved
510         */
511        public synchronized void save( UserProfile profile ) throws WikiSecurityException
512        {
513            if ( c_dom == null )
514            {
515                log.fatal( "Could not save profile " + profile + " database does not exist" );
516                throw new IllegalStateException( "FATAL: database does not exist" );
517            }
518            
519            checkForRefresh();
520            
521            DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
522            String index = profile.getLoginName();
523            NodeList users = c_dom.getElementsByTagName( USER_TAG );
524            Element user = null;
525            for( int i = 0; i < users.getLength(); i++ )
526            {
527                Element currentUser = (Element) users.item( i );
528                if ( currentUser.getAttribute( LOGIN_NAME ).equals( index ) )
529                {
530                    user = currentUser;
531                    break;
532                }
533            }
534            
535            boolean isNew = false;
536            
537            Date modDate = new Date( System.currentTimeMillis() );
538            if( user == null )
539            {
540                // Create new user node
541                profile.setCreated( modDate );
542                log.info( "Creating new user " + index );
543                user = c_dom.createElement( USER_TAG );
544                c_dom.getDocumentElement().appendChild( user );
545                setAttribute( user, CREATED, c_format.format( profile.getCreated() ) );
546                isNew = true;
547            }
548            else
549            {
550                // To update existing user node, delete old attributes first...
551                NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
552                for ( int i = 0; i < attributes.getLength(); i++ )
553                {
554                    user.removeChild( attributes.item( i ) );
555                }
556            }
557            
558            setAttribute( user, UID, profile.getUid() );
559            setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) );
560            setAttribute( user, LOGIN_NAME, profile.getLoginName() );
561            setAttribute( user, FULL_NAME, profile.getFullname() );
562            setAttribute( user, WIKI_NAME, profile.getWikiName() );
563            setAttribute( user, EMAIL, profile.getEmail() );
564            Date lockExpiry = profile.getLockExpiry();
565            setAttribute( user, LOCK_EXPIRY, lockExpiry == null ? "" : c_format.format( lockExpiry ) );
566    
567            // Hash and save the new password if it's different from old one
568            String newPassword = profile.getPassword();
569            if ( newPassword != null && !newPassword.equals( "" ) )
570            {
571                String oldPassword = user.getAttribute( PASSWORD );
572                if ( !oldPassword.equals( newPassword ) )
573                {
574                    setAttribute( user, PASSWORD, getHash( newPassword ) );
575                }
576            }
577            
578            // Save the attributes as as Base64 string
579            if ( profile.getAttributes().size() > 0 )
580            {
581                try
582                {
583                    String encodedAttributes = Serializer.serializeToBase64( profile.getAttributes() );
584                    Element attributes = c_dom.createElement( ATTRIBUTES_TAG );
585                    user.appendChild( attributes );
586                    Text value = c_dom.createTextNode( encodedAttributes );
587                    attributes.appendChild( value );
588                }
589                catch ( IOException e )
590                {
591                    throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
592                }
593            }
594    
595            // Set the profile timestamps
596            if ( isNew )
597            {
598                profile.setCreated( modDate );
599            }
600            profile.setLastModified( modDate );
601            
602            // Commit to disk
603            saveDOM();
604        }
605    
606        /**
607         * Private method that returns the first {@link UserProfile}matching a
608         * &lt;user&gt; element's supplied attribute. This method will also
609         * set the UID if it has not yet been set.
610         * @param matchAttribute
611         * @param index
612         * @return the profile, or <code>null</code> if not found
613         */
614        private UserProfile findByAttribute( String matchAttribute, String index )
615        {
616            if ( c_dom == null )
617            {
618                throw new IllegalStateException( "FATAL: database does not exist" );
619            }
620            
621            checkForRefresh();
622            
623            NodeList users = c_dom.getElementsByTagName( USER_TAG );
624            
625            if( users == null ) return null;
626            
627            for( int i = 0; i < users.getLength(); i++ )
628            {
629                Element user = (Element) users.item( i );
630                if ( user.getAttribute( matchAttribute ).equals( index ) )
631                {
632                    UserProfile profile = newProfile();
633                    
634                    // Parse basic attributes
635                    profile.setUid( user.getAttribute( UID ) );
636                    if ( profile.getUid() == null || profile.getUid().length() == 0 )
637                    {
638                        profile.setUid( generateUid( this ) );
639                    }
640                    profile.setLoginName( user.getAttribute( LOGIN_NAME ) );
641                    profile.setFullname( user.getAttribute( FULL_NAME ) );
642                    profile.setPassword( user.getAttribute( PASSWORD ) );
643                    profile.setEmail( user.getAttribute( EMAIL ) );
644                    
645                    // Get created/modified timestamps
646                    String created = user.getAttribute( CREATED );
647                    String modified = user.getAttribute( LAST_MODIFIED );
648                    profile.setCreated( parseDate( profile, created ) );                  
649                    profile.setLastModified( parseDate( profile, modified ) );                  
650                    
651                    // Is the profile locked?
652                    String lockExpiry = user.getAttribute( LOCK_EXPIRY );
653                    if ( lockExpiry == null || lockExpiry.length() == 0 )
654                    {
655                        profile.setLockExpiry( null );
656                    }
657                    else
658                    {
659                        profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) );
660                    }
661                    
662                    // Extract all of the user's attributes (should only be one attributes tag, but you never know!)
663                    NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
664                    for ( int j = 0; j < attributes.getLength(); j++ )
665                    {
666                        Element attribute = (Element)attributes.item( j );
667                        String serializedMap = extractText( attribute );
668                        try
669                        {
670                            Map<String,? extends Serializable> map = Serializer.deserializeFromBase64( serializedMap );
671                            profile.getAttributes().putAll( map );
672                        }
673                        catch ( IOException e )
674                        {
675                            log.error( "Could not parse user profile attributes!", e );
676                        }
677                    }
678    
679                    return profile;
680                }
681            }
682            return null;
683        }
684    
685        /**
686         * Extracts all of the text nodes that are immediate children of an Element.
687         * @param element the base element
688         * @return the text nodes that are immediate children of the base element, concatenated together
689         */
690        private String extractText( Element element )
691        {
692            String text = "";
693            if ( element.getChildNodes().getLength() > 0 )
694            {
695                NodeList children = element.getChildNodes();
696                for ( int k = 0; k < children.getLength(); k++ )
697                {
698                    Node child = children.item( k );
699                    if ( child.getNodeType() == Node.TEXT_NODE )
700                    {
701                        text = text + ((Text)child).getData();
702                    }
703                }
704            }
705            return text;
706        }
707    
708        /**
709         *  Tries to parse a date using the default format - then, for backwards
710         *  compatibility reasons, tries the platform default.
711         *  
712         *  @param profile
713         *  @param date
714         *  @return A parsed date, or null, if both parse attempts fail.
715         */
716        private Date parseDate( UserProfile profile, String date )
717        {
718            try
719            {
720                DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
721                return c_format.parse( date );
722            }
723            catch( ParseException e )
724            {
725                try
726                {
727                    return DateFormat.getDateTimeInstance().parse( date );
728                }
729                catch ( ParseException e2)
730                {
731                    log.warn("Could not parse 'created' or 'lastModified' "
732                        + "attribute for "
733                        + " profile '" + profile.getLoginName() + "'."
734                        + " It may have been tampered with." );
735                }            
736            }
737            return null;
738        }
739        
740        /**
741         * After loading the DOM, this method sanity-checks the dates in the DOM and makes
742         * sure they are formatted properly. This is sort-of hacky, but it should work.
743         */
744        private void sanitizeDOM()
745        {
746            if ( c_dom == null )
747            {
748                throw new IllegalStateException( "FATAL: database does not exist" );
749            }
750            
751            NodeList users = c_dom.getElementsByTagName( USER_TAG );
752            for( int i = 0; i < users.getLength(); i++ )
753            {
754                Element user = (Element) users.item( i );
755                
756                // Sanitize UID (and generate a new one if one does not exist)
757                String uid = user.getAttribute( UID ).trim();
758                if ( uid == null || uid.length() == 0 || "-1".equals( uid ) )
759                {
760                    uid = String.valueOf( generateUid( this ) );
761                    user.setAttribute( UID, uid );
762                }
763                
764                // Sanitize dates
765                String loginName = user.getAttribute( LOGIN_NAME );
766                String created = user.getAttribute( CREATED );
767                String modified = user.getAttribute( LAST_MODIFIED );
768                DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
769                try
770                {
771                    created = c_format.format( c_format.parse( created ) );
772                    modified = c_format.format( c_format.parse( modified ) );
773                    user.setAttribute( CREATED,  created );
774                    user.setAttribute( LAST_MODIFIED,  modified );
775                }
776                catch( ParseException e )
777                {
778                    try
779                    {
780                        created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) );
781                        modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) );
782                        user.setAttribute( CREATED,  created );
783                        user.setAttribute( LAST_MODIFIED,  modified );
784                    }
785                    catch ( ParseException e2 )
786                    {
787                        log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'."
788                                + " It may have been tampered with." );
789                    }            
790                }
791            }
792        }
793        
794        /**
795         * Private method that sets an attribute value for a supplied DOM element.
796         * @param element the element whose attribute is to be set
797         * @param attribute the name of the attribute to set
798         * @param value the desired attribute value
799         */
800        private void setAttribute( Element element, String attribute, String value ) {
801            if( value != null ) {
802                element.setAttribute( attribute, value );
803            }
804        }
805    }