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><user></code> elements under the root. User profile properties are attributes of the element. For example:</p> 059 * <blockquote><code> 060 * <users><br/> 061 * <user loginName="janne" fullName="Janne Jalkanen"<br/> 062 * wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/> 063 * password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/><br/> 064 * </users> 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 <user> 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}