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 NoSuchPrincipalException, 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< Principal > 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( wikiName == null ) { 163 log.warn( "Detected null wiki name in XMLUserDataBase. Check your user database." ); 164 } else { 165 final Principal 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 try { 206 c_dom = factory.newDocumentBuilder().parse( c_file ); 207 log.debug( "Database successfully initialized" ); 208 c_lastModified = c_file.lastModified(); 209 c_lastCheck = System.currentTimeMillis(); 210 } catch( final ParserConfigurationException e ) { 211 log.error( "Configuration error: " + e.getMessage() ); 212 } catch( final SAXException e ) { 213 log.error( "SAX error: " + e.getMessage() ); 214 } catch( final FileNotFoundException e ) { 215 log.info( "User database not found; creating from scratch..." ); 216 } catch( final IOException e ) { 217 log.error( "IO error: " + e.getMessage() ); 218 } 219 if( c_dom == null ) { 220 try { 221 // Create the DOM from scratch 222 c_dom = factory.newDocumentBuilder().newDocument(); 223 c_dom.appendChild( c_dom.createElement( "users" ) ); 224 } catch( final ParserConfigurationException e ) { 225 log.fatal( "Could not create in-memory DOM" ); 226 } 227 } 228 } 229 230 private void saveDOM() throws WikiSecurityException { 231 if( c_dom == null ) { 232 throw new IllegalStateException( "FATAL: database does not exist" ); 233 } 234 235 final File newFile = new File( c_file.getAbsolutePath() + ".new" ); 236 try( final BufferedWriter io = new BufferedWriter( new OutputStreamWriter( Files.newOutputStream( newFile.toPath() ), StandardCharsets.UTF_8 ) ) ) { 237 238 // Write the file header and document root 239 io.write( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" ); 240 io.write( "<users>\n" ); 241 242 // Write each profile as a <user> node 243 final Element root = c_dom.getDocumentElement(); 244 final NodeList nodes = root.getElementsByTagName( USER_TAG ); 245 for( int i = 0; i < nodes.getLength(); i++ ) { 246 final Element user = ( Element )nodes.item( i ); 247 io.write( " <" + USER_TAG + " " ); 248 io.write( UID ); 249 io.write( "=\"" + user.getAttribute( UID ) + "\" " ); 250 io.write( LOGIN_NAME ); 251 io.write( "=\"" + user.getAttribute( LOGIN_NAME ) + "\" " ); 252 io.write( WIKI_NAME ); 253 io.write( "=\"" + user.getAttribute( WIKI_NAME ) + "\" " ); 254 io.write( FULL_NAME ); 255 io.write( "=\"" + user.getAttribute( FULL_NAME ) + "\" " ); 256 io.write( EMAIL ); 257 io.write( "=\"" + user.getAttribute( EMAIL ) + "\" " ); 258 io.write( PASSWORD ); 259 io.write( "=\"" + user.getAttribute( PASSWORD ) + "\" " ); 260 io.write( CREATED ); 261 io.write( "=\"" + user.getAttribute( CREATED ) + "\" " ); 262 io.write( LAST_MODIFIED ); 263 io.write( "=\"" + user.getAttribute( LAST_MODIFIED ) + "\" " ); 264 io.write( LOCK_EXPIRY ); 265 io.write( "=\"" + user.getAttribute( LOCK_EXPIRY ) + "\" " ); 266 io.write( ">" ); 267 final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); 268 for( int j = 0; j < attributes.getLength(); j++ ) { 269 final Element attribute = ( Element )attributes.item( j ); 270 final String value = extractText( attribute ); 271 io.write( "\n <" + ATTRIBUTES_TAG + ">" ); 272 io.write( value ); 273 io.write( "</" + ATTRIBUTES_TAG + ">" ); 274 } 275 io.write( "\n </" + USER_TAG + ">\n" ); 276 } 277 io.write( "</users>" ); 278 } catch( final IOException e ) { 279 throw new WikiSecurityException( e.getLocalizedMessage(), e ); 280 } 281 282 // Copy new file over old version 283 final File backup = new File( c_file.getAbsolutePath() + ".old" ); 284 if( backup.exists() ) { 285 if( !backup.delete() ) { 286 log.error( "Could not delete old user database backup: " + backup ); 287 } 288 } 289 if( !c_file.renameTo( backup ) ) { 290 log.error( "Could not create user database backup: " + backup ); 291 } 292 if( !newFile.renameTo( c_file ) ) { 293 log.error( "Could not save database: " + backup + " restoring backup." ); 294 if( !backup.renameTo( c_file ) ) { 295 log.error( "Restore failed. Check the file permissions." ); 296 } 297 log.error( "Could not save database: " + c_file + ". Check the file permissions" ); 298 } 299 } 300 301 private long c_lastCheck; 302 private long c_lastModified; 303 304 private void checkForRefresh() { 305 final long time = System.currentTimeMillis(); 306 if( time - c_lastCheck > 60 * 1000L ) { 307 final long lastModified = c_file.lastModified(); 308 309 if( lastModified > c_lastModified ) { 310 buildDOM(); 311 } 312 } 313 } 314 315 /** 316 * {@inheritDoc} 317 * 318 * @see org.apache.wiki.auth.user.UserDatabase#rename(String, String) 319 */ 320 @Override 321 public synchronized void rename( final String loginName, final String newName) throws NoSuchPrincipalException, DuplicateUserException, WikiSecurityException { 322 if( c_dom == null ) { 323 log.fatal( "Could not rename profile '" + loginName + "'; database does not exist" ); 324 throw new IllegalStateException( "FATAL: database does not exist" ); 325 } 326 checkForRefresh(); 327 328 // Get the existing user; if not found, throws NoSuchPrincipalException 329 final UserProfile profile = findByLoginName( loginName ); 330 331 // Get user with the proposed name; if found, it's a collision 332 try { 333 final UserProfile otherProfile = findByLoginName( newName ); 334 if( otherProfile != null ) { 335 throw new DuplicateUserException( "security.error.cannot.rename", newName ); 336 } 337 } catch( final NoSuchPrincipalException e ) { 338 // Good! That means it's safe to save using the new name 339 } 340 341 // Find the user with the old login id attribute, and change it 342 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 343 for( int i = 0; i < users.getLength(); i++ ) { 344 final Element user = ( Element )users.item( i ); 345 if( user.getAttribute( LOGIN_NAME ).equals( loginName ) ) { 346 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 347 final Date modDate = new Date( System.currentTimeMillis() ); 348 setAttribute( user, LOGIN_NAME, newName ); 349 setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) ); 350 profile.setLoginName( newName ); 351 profile.setLastModified( modDate ); 352 break; 353 } 354 } 355 356 // Commit to disk 357 saveDOM(); 358 } 359 360 /** {@inheritDoc} */ 361 @Override 362 public synchronized void save( final UserProfile profile ) throws WikiSecurityException { 363 if ( c_dom == null ) { 364 log.fatal( "Could not save profile " + profile + " database does not exist" ); 365 throw new IllegalStateException( "FATAL: database does not exist" ); 366 } 367 368 checkForRefresh(); 369 370 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 371 final String index = profile.getLoginName(); 372 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 373 Element user = null; 374 for( int i = 0; i < users.getLength(); i++ ) { 375 final Element currentUser = ( Element )users.item( i ); 376 if( currentUser.getAttribute( LOGIN_NAME ).equals( index ) ) { 377 user = currentUser; 378 break; 379 } 380 } 381 382 boolean isNew = false; 383 384 final Date modDate = new Date( System.currentTimeMillis() ); 385 if( user == null ) { 386 // Create new user node 387 profile.setCreated( modDate ); 388 log.info( "Creating new user " + index ); 389 user = c_dom.createElement( USER_TAG ); 390 c_dom.getDocumentElement().appendChild( user ); 391 setAttribute( user, CREATED, c_format.format( profile.getCreated() ) ); 392 isNew = true; 393 } else { 394 // To update existing user node, delete old attributes first... 395 final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); 396 for( int i = 0; i < attributes.getLength(); i++ ) { 397 user.removeChild( attributes.item( i ) ); 398 } 399 } 400 401 setAttribute( user, UID, profile.getUid() ); 402 setAttribute( user, LAST_MODIFIED, c_format.format( modDate ) ); 403 setAttribute( user, LOGIN_NAME, profile.getLoginName() ); 404 setAttribute( user, FULL_NAME, profile.getFullname() ); 405 setAttribute( user, WIKI_NAME, profile.getWikiName() ); 406 setAttribute( user, EMAIL, profile.getEmail() ); 407 final Date lockExpiry = profile.getLockExpiry(); 408 setAttribute( user, LOCK_EXPIRY, lockExpiry == null ? "" : c_format.format( lockExpiry ) ); 409 410 // Hash and save the new password if it's different from old one 411 final String newPassword = profile.getPassword(); 412 if( newPassword != null && !newPassword.equals( "" ) ) { 413 final String oldPassword = user.getAttribute( PASSWORD ); 414 if( !oldPassword.equals( newPassword ) ) { 415 setAttribute( user, PASSWORD, getHash( newPassword ) ); 416 } 417 } 418 419 // Save the attributes as as Base64 string 420 if( profile.getAttributes().size() > 0 ) { 421 try { 422 final String encodedAttributes = Serializer.serializeToBase64( profile.getAttributes() ); 423 final Element attributes = c_dom.createElement( ATTRIBUTES_TAG ); 424 user.appendChild( attributes ); 425 final Text value = c_dom.createTextNode( encodedAttributes ); 426 attributes.appendChild( value ); 427 } catch( final IOException e ) { 428 throw new WikiSecurityException( "Could not save user profile attribute. Reason: " + e.getMessage(), e ); 429 } 430 } 431 432 // Set the profile timestamps 433 if( isNew ) { 434 profile.setCreated( modDate ); 435 } 436 profile.setLastModified( modDate ); 437 438 // Commit to disk 439 saveDOM(); 440 } 441 442 /** 443 * Private method that returns the first {@link UserProfile}matching a <user> element's supplied attribute. This method will also 444 * set the UID if it has not yet been set. 445 * 446 * @param matchAttribute 447 * @param index 448 * @return the profile, or <code>null</code> if not found 449 */ 450 private UserProfile findByAttribute( final String matchAttribute, String index ) { 451 if ( c_dom == null ) { 452 throw new IllegalStateException( "FATAL: database does not exist" ); 453 } 454 455 checkForRefresh(); 456 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 457 if( users == null ) { 458 return null; 459 } 460 461 // check if we have to do a case insensitive compare 462 boolean caseSensitiveCompare = !matchAttribute.equals( EMAIL ); 463 464 for( int i = 0; i < users.getLength(); i++ ) { 465 final Element user = (Element) users.item( i ); 466 String userAttribute = user.getAttribute( matchAttribute ); 467 if( !caseSensitiveCompare ) { 468 userAttribute = StringUtils.lowerCase(userAttribute); 469 index = StringUtils.lowerCase(index); 470 } 471 if( userAttribute.equals( index ) ) { 472 final UserProfile profile = newProfile(); 473 474 // Parse basic attributes 475 profile.setUid( user.getAttribute( UID ) ); 476 if( profile.getUid() == null || profile.getUid().isEmpty() ) { 477 profile.setUid( generateUid( this ) ); 478 } 479 profile.setLoginName( user.getAttribute( LOGIN_NAME ) ); 480 profile.setFullname( user.getAttribute( FULL_NAME ) ); 481 profile.setPassword( user.getAttribute( PASSWORD ) ); 482 profile.setEmail( user.getAttribute( EMAIL ) ); 483 484 // Get created/modified timestamps 485 final String created = user.getAttribute( CREATED ); 486 final String modified = user.getAttribute( LAST_MODIFIED ); 487 profile.setCreated( parseDate( profile, created ) ); 488 profile.setLastModified( parseDate( profile, modified ) ); 489 490 // Is the profile locked? 491 final String lockExpiry = user.getAttribute( LOCK_EXPIRY ); 492 if( lockExpiry == null || lockExpiry.isEmpty() ) { 493 profile.setLockExpiry( null ); 494 } else { 495 profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) ); 496 } 497 498 // Extract all of the user's attributes (should only be one attributes tag, but you never know!) 499 final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG ); 500 for( int j = 0; j < attributes.getLength(); j++ ) { 501 final Element attribute = ( Element )attributes.item( j ); 502 final String serializedMap = extractText( attribute ); 503 try { 504 final Map< String, ? extends Serializable > map = Serializer.deserializeFromBase64( serializedMap ); 505 profile.getAttributes().putAll( map ); 506 } catch( final IOException e ) { 507 log.error( "Could not parse user profile attributes!", e ); 508 } 509 } 510 511 return profile; 512 } 513 } 514 return null; 515 } 516 517 /** 518 * Extracts all of the text nodes that are immediate children of an Element. 519 * 520 * @param element the base element 521 * @return the text nodes that are immediate children of the base element, concatenated together 522 */ 523 private String extractText( final Element element ) { 524 final StringBuilder text = new StringBuilder(); 525 if( element.getChildNodes().getLength() > 0 ) { 526 final NodeList children = element.getChildNodes(); 527 for( int k = 0; k < children.getLength(); k++ ) { 528 final Node child = children.item( k ); 529 if( child.getNodeType() == Node.TEXT_NODE ) { 530 text.append(((Text) child).getData()); 531 } 532 } 533 } 534 return text.toString(); 535 } 536 537 /** 538 * Tries to parse a date using the default format - then, for backwards compatibility reasons, tries the platform default. 539 * 540 * @param profile 541 * @param date 542 * @return A parsed date, or null, if both parse attempts fail. 543 */ 544 private Date parseDate( final UserProfile profile, final String date ) { 545 try { 546 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 547 return c_format.parse( date ); 548 } catch( final ParseException e ) { 549 try { 550 return DateFormat.getDateTimeInstance().parse( date ); 551 } catch( final ParseException e2 ) { 552 log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + profile.getLoginName() + "'." + 553 " It may have been tampered with.", e2 ); 554 } 555 } 556 return null; 557 } 558 559 /** 560 * After loading the DOM, this method sanity-checks the dates in the DOM and makes sure they are formatted properly. This is sort-of 561 * hacky, but it should work. 562 */ 563 private void sanitizeDOM() { 564 if( c_dom == null ) { 565 throw new IllegalStateException( "FATAL: database does not exist" ); 566 } 567 568 final NodeList users = c_dom.getElementsByTagName( USER_TAG ); 569 for( int i = 0; i < users.getLength(); i++ ) { 570 final Element user = ( Element )users.item( i ); 571 572 // Sanitize UID (and generate a new one if one does not exist) 573 String uid = user.getAttribute( UID ).trim(); 574 if( uid == null || uid.isEmpty() || "-1".equals( uid ) ) { 575 uid = String.valueOf( generateUid( this ) ); 576 user.setAttribute( UID, uid ); 577 } 578 579 // Sanitize dates 580 final String loginName = user.getAttribute( LOGIN_NAME ); 581 String created = user.getAttribute( CREATED ); 582 String modified = user.getAttribute( LAST_MODIFIED ); 583 final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT ); 584 try { 585 created = c_format.format( c_format.parse( created ) ); 586 modified = c_format.format( c_format.parse( modified ) ); 587 user.setAttribute( CREATED, created ); 588 user.setAttribute( LAST_MODIFIED, modified ); 589 } catch( final ParseException e ) { 590 try { 591 created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) ); 592 modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) ); 593 user.setAttribute( CREATED, created ); 594 user.setAttribute( LAST_MODIFIED, modified ); 595 } catch( final ParseException e2 ) { 596 log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'." 597 + " It may have been tampered with." ); 598 } 599 } 600 } 601 } 602 603 /** 604 * Private method that sets an attribute value for a supplied DOM element. 605 * 606 * @param element the element whose attribute is to be set 607 * @param attribute the name of the attribute to set 608 * @param value the desired attribute value 609 */ 610 private void setAttribute( final Element element, final String attribute, final String value ) { 611 if( value != null ) { 612 element.setAttribute( attribute, value ); 613 } 614 } 615 616}