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