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.api.core.Engine;
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.IOException;
042import java.io.OutputStreamWriter;
043import java.io.Serializable;
044import java.nio.charset.StandardCharsets;
045import java.nio.file.Files;
046import java.security.Principal;
047import java.text.DateFormat;
048import java.text.ParseException;
049import java.text.SimpleDateFormat;
050import java.util.Date;
051import java.util.Map;
052import java.util.Properties;
053import java.util.SortedSet;
054import java.util.TreeSet;
055
056/**
057 * <p>Manages {@link DefaultUserProfile} objects using XML files for persistence. Passwords are hashed using SHA1. User entries are simple
058 * <code>&lt;user&gt;</code> elements under the root. User profile properties are attributes of the 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.
072public class XMLUserDatabase extends AbstractUserDatabase {
073
074    /** The jspwiki.properties property specifying the file system location of the user database. */
075    public static final String  PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile";
076    private static final String DEFAULT_USERDATABASE = "userdatabase.xml";
077    private static final String ATTRIBUTES_TAG    = "attributes";
078    private static final String CREATED           = "created";
079    private static final String EMAIL             = "email";
080    private static final String FULL_NAME         = "fullName";
081    private static final String LOGIN_NAME        = "loginName";
082    private static final String LAST_MODIFIED     = "lastModified";
083    private static final String LOCK_EXPIRY       = "lockExpiry";
084    private static final String PASSWORD          = "password";
085    private static final String UID               = "uid";
086    private static final String USER_TAG          = "user";
087    private static final String WIKI_NAME         = "wikiName";
088    private static final String DATE_FORMAT       = "yyyy.MM.dd 'at' HH:mm:ss:SSS z";
089    private Document            c_dom;
090    private File                c_file;
091
092    /** {@inheritDoc} */
093    @Override
094    public synchronized void deleteByLoginName( final String loginName ) throws WikiSecurityException {
095        if( c_dom == null ) {
096            throw new WikiSecurityException( "FATAL: database does not exist" );
097        }
098
099        final NodeList users = c_dom.getDocumentElement().getElementsByTagName( USER_TAG );
100        for( int i = 0; i < users.getLength(); i++ ) {
101            final Element user = ( Element )users.item( i );
102            if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) {
103                c_dom.getDocumentElement().removeChild( user );
104
105                // Commit to disk
106                saveDOM();
107                return;
108            }
109        }
110        throw new NoSuchPrincipalException( "Not in database: " + loginName );
111    }
112
113    /** {@inheritDoc} */
114    @Override
115    public UserProfile findByEmail( final String index ) throws NoSuchPrincipalException {
116        return findBy( EMAIL, index );
117    }
118
119    /** {@inheritDoc} */
120    @Override
121    public UserProfile findByFullName( final String index ) throws NoSuchPrincipalException {
122        return findBy( FULL_NAME, index );
123    }
124
125    /** {@inheritDoc} */
126    @Override
127    public UserProfile findByLoginName( final String index ) throws NoSuchPrincipalException {
128        return findBy( LOGIN_NAME, index );
129    }
130
131    /** {@inheritDoc} */
132    @Override
133    public UserProfile findByUid( final String uid ) throws NoSuchPrincipalException {
134        return findBy( UID, uid );
135    }
136
137    /** {@inheritDoc} */
138    @Override
139    public UserProfile findByWikiName( final String index ) throws NoSuchPrincipalException {
140        return findBy( WIKI_NAME, index );
141    }
142
143    public UserProfile findBy( final String attr, final String value ) throws NoSuchPrincipalException {
144        final UserProfile profile = findByAttribute( attr, value );
145        if ( profile != null ) {
146            return profile;
147        }
148        throw new NoSuchPrincipalException( "Not in database: " + value );
149    }
150
151    /** {@inheritDoc} */
152    @Override
153    public Principal[] getWikiNames() throws WikiSecurityException {
154        if ( c_dom == null ) {
155            throw new IllegalStateException( "FATAL: database does not exist" );
156        }
157        final SortedSet< WikiPrincipal > principals = new TreeSet<>();
158        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
159        for( int i = 0; i < users.getLength(); i++ ) {
160            final Element user = ( Element )users.item( i );
161            final String wikiName = user.getAttribute( WIKI_NAME );
162            if( StringUtils.isEmpty( wikiName ) ) {
163                log.warn( "Detected null or empty wiki name for {} in XMLUserDataBase. Check your user database.", user.getAttribute( LOGIN_NAME ) );
164            } else {
165                final WikiPrincipal principal = new WikiPrincipal( wikiName, WikiPrincipal.WIKI_NAME );
166                principals.add( principal );
167            }
168        }
169        return principals.toArray( new Principal[0] );
170    }
171
172    /** {@inheritDoc} */
173    @Override
174    public void initialize( final Engine engine, final Properties props ) throws NoRequiredPropertyException {
175        final File defaultFile;
176        if( engine.getRootPath() == null ) {
177            log.warn( "Cannot identify JSPWiki root path" );
178            defaultFile = new File( "WEB-INF/" + DEFAULT_USERDATABASE ).getAbsoluteFile();
179        } else {
180            defaultFile = new File( engine.getRootPath() + "/WEB-INF/" + DEFAULT_USERDATABASE );
181        }
182
183        // Get database file location
184        final String file = TextUtil.getStringProperty( props, PROP_USERDATABASE, defaultFile.getAbsolutePath() );
185        if( file == null ) {
186            log.warn( "XML user database property " + PROP_USERDATABASE + " not found; trying " + defaultFile );
187            c_file = defaultFile;
188        } else {
189            c_file = new File( file );
190        }
191
192        log.info( "XML user database at " + c_file.getAbsolutePath() );
193
194        buildDOM();
195        sanitizeDOM();
196    }
197
198    private void buildDOM() {
199        // Read DOM
200        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
201        factory.setValidating( false );
202        factory.setExpandEntityReferences( false );
203        factory.setIgnoringComments( true );
204        factory.setNamespaceAware( false );
205        //factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_DTD, "" );
206        //factory.setAttribute( XMLConstants.ACCESS_EXTERNAL_SCHEMA, "" );
207        try {
208            c_dom = factory.newDocumentBuilder().parse( c_file );
209            log.debug( "Database successfully initialized" );
210            c_lastModified = c_file.lastModified();
211            c_lastCheck = System.currentTimeMillis();
212        } catch( final ParserConfigurationException e ) {
213            log.error( "Configuration error: {}", e.getMessage() );
214        } catch( final SAXException e ) {
215            log.error( "SAX error: {}", e.getMessage() );
216        } catch( final FileNotFoundException e ) {
217            log.info( "User database not found; creating from scratch..." );
218        } catch( final IOException e ) {
219            log.error( "IO error: {}", e.getMessage() );
220        }
221        if( c_dom == null ) {
222            try {
223                //  Create the DOM from scratch
224                c_dom = factory.newDocumentBuilder().newDocument();
225                c_dom.appendChild( c_dom.createElement( "users" ) );
226            } catch( final ParserConfigurationException e ) {
227                log.fatal( "Could not create in-memory DOM" );
228            }
229        }
230    }
231
232    private void saveDOM() throws WikiSecurityException {
233        if( c_dom == null ) {
234            throw new IllegalStateException( "FATAL: database does not exist" );
235        }
236
237        final File newFile = new File( c_file.getAbsolutePath() + ".new" );
238        try( final BufferedWriter io = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( newFile.toPath() ), StandardCharsets.UTF_8 ) ) ) {
239
240            // Write the file header and document root
241            io.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" );
242            io.write( "<users>\n" );
243
244            // Write each profile as a <user> node
245            final Element root = c_dom.getDocumentElement();
246            final NodeList nodes = root.getElementsByTagName( USER_TAG );
247            for( int i = 0; i < nodes.getLength(); i++ ) {
248                final Element user = ( Element )nodes.item( i );
249                io.write( "    <" + USER_TAG + " " );
250                io.write( UID );
251                io.write( "=\"" + user.getAttribute( UID ) + "\" " );
252                io.write( LOGIN_NAME );
253                io.write( "=\"" + user.getAttribute( LOGIN_NAME ) + "\" " );
254                io.write( WIKI_NAME );
255                io.write( "=\"" + user.getAttribute( WIKI_NAME ) + "\" " );
256                io.write( FULL_NAME );
257                io.write( "=\"" + user.getAttribute( FULL_NAME ) + "\" " );
258                io.write( EMAIL );
259                io.write( "=\"" + user.getAttribute( EMAIL ) + "\" " );
260                io.write( PASSWORD );
261                io.write( "=\"" + user.getAttribute( PASSWORD ) + "\" " );
262                io.write( CREATED );
263                io.write( "=\"" + user.getAttribute( CREATED ) + "\" " );
264                io.write( LAST_MODIFIED );
265                io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " );
266                io.write( LOCK_EXPIRY );
267                io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " );
268                io.write( ">" );
269                final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
270                for( int j = 0; j < attributes.getLength(); j++ ) {
271                    final Element attribute = ( Element )attributes.item( j );
272                    final String value = extractText( attribute );
273                    io.write( "\n        <" + ATTRIBUTES_TAG + ">" );
274                    io.write( value );
275                    io.write( "</" + ATTRIBUTES_TAG + ">" );
276                }
277                io.write( "\n    </" + USER_TAG + ">\n" );
278            }
279            io.write( "</users>" );
280        } catch( final IOException e ) {
281            throw new WikiSecurityException( e.getLocalizedMessage(), e );
282        }
283
284        // Copy new file over old version
285        final File backup = new File( c_file.getAbsolutePath() + ".old" );
286        if( backup.exists() ) {
287            if( !backup.delete() ) {
288                log.error( "Could not delete old user database backup: " + backup );
289            }
290        }
291        if( !c_file.renameTo( backup ) ) {
292            log.error( "Could not create user database backup: " + backup );
293        }
294        if( !newFile.renameTo( c_file ) ) {
295            log.error( "Could not save database: " + backup + " restoring backup." );
296            if( !backup.renameTo( c_file ) ) {
297                log.error( "Restore failed. Check the file permissions." );
298            }
299            log.error( "Could not save database: " + c_file + ". Check the file permissions" );
300        }
301    }
302
303    private long c_lastCheck;
304    private long c_lastModified;
305
306    private void checkForRefresh() {
307        final long time = System.currentTimeMillis();
308        if( time - c_lastCheck > 60 * 1000L ) {
309            final long lastModified = c_file.lastModified();
310
311            if( lastModified > c_lastModified ) {
312                buildDOM();
313            }
314        }
315    }
316
317    /**
318     * {@inheritDoc}
319     *
320     * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String)
321     */
322    @Override
323    public synchronized void rename( final String loginName, final String newName) throws DuplicateUserException, WikiSecurityException {
324        if( c_dom == null ) {
325            log.fatal( "Could not rename profile '" + loginName + "'; database does not exist" );
326            throw new IllegalStateException( "FATAL: database does not exist" );
327        }
328        checkForRefresh();
329
330        // Get the existing user; if not found, throws NoSuchPrincipalException
331        final UserProfile profile = findByLoginName( loginName );
332
333        // Get user with the proposed name; if found, it's a collision
334        try {
335            final UserProfile otherProfile = findByLoginName( newName );
336            if( otherProfile != null ) {
337                throw new DuplicateUserException( "security.error.cannot.rename", newName );
338            }
339        } catch( final NoSuchPrincipalException e ) {
340            // Good! That means it's safe to save using the new name
341        }
342
343        // Find the user with the old login id attribute, and change it
344        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
345        for( int i = 0; i < users.getLength(); i++ ) {
346            final Element user = ( Element )users.item( i );
347            if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) {
348                final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
349                final Date modDate = new Date( System.currentTimeMillis() );
350                setAttribute( user, LOGIN_NAME, newName );
351                setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) );
352                profile.setLoginName( newName );
353                profile.setLastModified( modDate );
354                break;
355            }
356        }
357
358        // Commit to disk
359        saveDOM();
360    }
361
362    /** {@inheritDoc} */
363    @Override
364    public synchronized void save( final UserProfile profile ) throws WikiSecurityException {
365        if ( c_dom == null ) {
366            log.fatal( "Could not save profile " + profile + " database does not exist" );
367            throw new IllegalStateException( "FATAL: database does not exist" );
368        }
369
370        checkForRefresh();
371
372        final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
373        final String index = profile.getLoginName();
374        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
375        Element user = null;
376        for( int i = 0; i < users.getLength(); i++ ) {
377            final Element currentUser = ( Element )users.item( i );
378            if( currentUser.getAttribute( LOGIN_NAME ).equals( index ) ) {
379                user = currentUser;
380                break;
381            }
382        }
383
384        boolean isNew = false;
385
386        final Date modDate = new Date( System.currentTimeMillis() );
387        if( user == null ) {
388            // Create new user node
389            profile.setCreated( modDate );
390            log.info( "Creating new user " + index );
391            user = c_dom.createElement( USER_TAG );
392            c_dom.getDocumentElement().appendChild( user );
393            setAttribute( user, CREATED, c_format.format( profile.getCreated() ) );
394            isNew = true;
395        } else {
396            // To update existing user node, delete old attributes first...
397            final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
398            for( int i = 0; i < attributes.getLength(); i++ ) {
399                user.removeChild( attributes.item( i ) );
400            }
401        }
402
403        setAttribute( user, UID, profile.getUid() );
404        setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) );
405        setAttribute( user, LOGIN_NAME, profile.getLoginName() );
406        setAttribute( user, FULL_NAME, profile.getFullname() );
407        setAttribute( user, WIKI_NAME, profile.getWikiName() );
408        setAttribute( user, EMAIL, profile.getEmail() );
409        final Date lockExpiry = profile.getLockExpiry();
410        setAttribute( user, LOCK_EXPIRY, lockExpiry == null ? "" : c_format.format( lockExpiry ) );
411
412        // Hash and save the new password if it's different from old one
413        final String newPassword = profile.getPassword();
414        if( newPassword != null && !newPassword.equals( "" ) ) {
415            final String oldPassword = user.getAttribute( PASSWORD );
416            if( !oldPassword.equals( newPassword ) ) {
417                setAttribute( user, PASSWORD, getHash( newPassword ) );
418            }
419        }
420
421        // Save the attributes as Base64 string
422        if( profile.getAttributes().size() > 0 ) {
423            try {
424                final String encodedAttributes = Serializer.serializeToBase64( profile.getAttributes() );
425                final Element attributes = c_dom.createElement( ATTRIBUTES_TAG );
426                user.appendChild( attributes );
427                final Text value = c_dom.createTextNode( encodedAttributes );
428                attributes.appendChild( value );
429            } catch( final IOException e ) {
430                throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e );
431            }
432        }
433
434        // Set the profile timestamps
435        if( isNew ) {
436            profile.setCreated( modDate );
437        }
438        profile.setLastModified( modDate );
439
440        // Commit to disk
441        saveDOM();
442    }
443
444    /**
445     * Private method that returns the first {@link UserProfile}matching a &lt;user&gt; element's supplied attribute. This method will also
446     * set the UID if it has not yet been set.
447     *
448     * @param matchAttribute matching attribute
449     * @param index value to match
450     * @return the profile, or <code>null</code> if not found
451     */
452    private UserProfile findByAttribute( final String matchAttribute, String index ) {
453        if ( c_dom == null ) {
454            throw new IllegalStateException( "FATAL: database does not exist" );
455        }
456
457        checkForRefresh();
458        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
459        if( users == null ) {
460            return null;
461        }
462
463        // check if we have to do a case-insensitive compare
464        final boolean caseSensitiveCompare = !matchAttribute.equals( EMAIL );
465
466        for( int i = 0; i < users.getLength(); i++ ) {
467            final Element user = (Element) users.item( i );
468            String userAttribute = user.getAttribute( matchAttribute );
469            if( !caseSensitiveCompare ) {
470                userAttribute = StringUtils.lowerCase(userAttribute);
471                index = StringUtils.lowerCase(index);
472            }
473            if( userAttribute.equals( index ) ) {
474                final UserProfile profile = newProfile();
475
476                // Parse basic attributes
477                profile.setUid( user.getAttribute( UID ) );
478                if( profile.getUid() == null || profile.getUid().isEmpty() ) {
479                    profile.setUid( generateUid( this ) );
480                }
481                profile.setLoginName( user.getAttribute( LOGIN_NAME ) );
482                profile.setFullname( user.getAttribute( FULL_NAME ) );
483                profile.setPassword( user.getAttribute( PASSWORD ) );
484                profile.setEmail( user.getAttribute( EMAIL ) );
485
486                // Get created/modified timestamps
487                final String created = user.getAttribute( CREATED );
488                final String modified = user.getAttribute( LAST_MODIFIED );
489                profile.setCreated( parseDate( profile, created ) );
490                profile.setLastModified( parseDate( profile, modified ) );
491
492                // Is the profile locked?
493                final String lockExpiry = user.getAttribute( LOCK_EXPIRY );
494                if( StringUtils.isEmpty( lockExpiry ) || lockExpiry.isEmpty() ) {
495                    profile.setLockExpiry( null );
496                } else {
497                    profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) );
498                }
499
500                // Extract all the user's attributes (should only be one attributes tag, but you never know!)
501                final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
502                for( int j = 0; j < attributes.getLength(); j++ ) {
503                    final Element attribute = ( Element )attributes.item( j );
504                    final String serializedMap = extractText( attribute );
505                    try {
506                        final Map< String, ? extends Serializable > map = Serializer.deserializeFromBase64( serializedMap );
507                        profile.getAttributes().putAll( map );
508                    } catch( final IOException e ) {
509                        log.error( "Could not parse user profile attributes!", e );
510                    }
511                }
512
513                return profile;
514            }
515        }
516        return null;
517    }
518
519    /**
520     * Extracts all the text nodes that are immediate children of an Element.
521     *
522     * @param element the base element
523     * @return the text nodes that are immediate children of the base element, concatenated together
524     */
525    private String extractText( final Element element ) {
526        final StringBuilder text = new StringBuilder();
527        if( element.getChildNodes().getLength() > 0 ) {
528            final NodeList children = element.getChildNodes();
529            for( int k = 0; k < children.getLength(); k++ ) {
530                final Node child = children.item( k );
531                if( child.getNodeType() == Node.TEXT_NODE ) {
532                    text.append(((Text) child).getData());
533                }
534            }
535        }
536        return text.toString();
537    }
538
539    /**
540     *  Tries to parse a date using the default format - then, for backwards compatibility reasons, tries the platform default.
541     *
542     *  @param profile profile associated to the date.
543     *  @param date date to be parsed.
544     *  @return A parsed date, or null, if both parse attempts fail.
545     */
546    private Date parseDate( final UserProfile profile, final String date ) {
547        try {
548            final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
549            return c_format.parse( date );
550        } catch( final ParseException e ) {
551            try {
552                return DateFormat.getDateTimeInstance().parse( date );
553            } catch( final ParseException e2 ) {
554                log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + profile.getLoginName() + "'." +
555                          " It may have been tampered with.", e2 );
556            }
557        }
558        return null;
559    }
560
561    /**
562     * After loading the DOM, this method sanity-checks the dates in the DOM and makes sure they are formatted properly. This is sort-of
563     * hacky, but it should work.
564     */
565    private void sanitizeDOM() {
566        if( c_dom == null ) {
567            throw new IllegalStateException( "FATAL: database does not exist" );
568        }
569
570        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
571        for( int i = 0; i < users.getLength(); i++ ) {
572            final Element user = ( Element )users.item( i );
573
574            // Sanitize UID (and generate a new one if one does not exist)
575            String uid = user.getAttribute( UID ).trim();
576            if( StringUtils.isEmpty( uid ) || "-1".equals( uid ) ) {
577                uid = String.valueOf( generateUid( this ) );
578                user.setAttribute( UID, uid );
579            }
580
581            // Sanitize dates
582            final String loginName = user.getAttribute( LOGIN_NAME );
583            String created = user.getAttribute( CREATED );
584            String modified = user.getAttribute( LAST_MODIFIED );
585            final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
586            try {
587                created = c_format.format( c_format.parse( created ) );
588                modified = c_format.format( c_format.parse( modified ) );
589                user.setAttribute( CREATED, created );
590                user.setAttribute( LAST_MODIFIED, modified );
591            } catch( final ParseException e ) {
592                try {
593                    created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) );
594                    modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) );
595                    user.setAttribute( CREATED, created );
596                    user.setAttribute( LAST_MODIFIED, modified );
597                } catch( final ParseException e2 ) {
598                    log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'."
599                            + " It may have been tampered with." );
600                }
601            }
602        }
603    }
604
605    /**
606     * Private method that sets an attribute value for a supplied DOM element.
607     *
608     * @param element the element whose attribute is to be set
609     * @param attribute the name of the attribute to set
610     * @param value the desired attribute value
611     */
612    private void setAttribute( final Element element, final String attribute, final String value ) {
613        if( value != null ) {
614            element.setAttribute( attribute, value );
615        }
616    }
617
618}