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