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