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.FileOutputStream;
042import java.io.IOException;
043import java.io.OutputStreamWriter;
044import java.io.Serializable;
045import java.nio.charset.StandardCharsets;
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             = null;
090    private File                c_file            = null;
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[principals.size()] );
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( new FileOutputStream( newFile ), 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    = 0;
302    private long c_lastModified = 0;
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 &lt;user&gt; 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 = true;
463        if (matchAttribute.equals(EMAIL)) {
464            caseSensitiveCompare = false;
465        }
466
467        for( int i = 0; i < users.getLength(); i++ ) {
468            final Element user = (Element) users.item( i );
469            String userAttribute = user.getAttribute( matchAttribute );
470            if( !caseSensitiveCompare ) {
471                userAttribute = StringUtils.lowerCase(userAttribute);
472                index = StringUtils.lowerCase(index);
473            }
474            if( userAttribute.equals( index ) ) {
475                final UserProfile profile = newProfile();
476
477                // Parse basic attributes
478                profile.setUid( user.getAttribute( UID ) );
479                if( profile.getUid() == null || profile.getUid().length() == 0 ) {
480                    profile.setUid( generateUid( this ) );
481                }
482                profile.setLoginName( user.getAttribute( LOGIN_NAME ) );
483                profile.setFullname( user.getAttribute( FULL_NAME ) );
484                profile.setPassword( user.getAttribute( PASSWORD ) );
485                profile.setEmail( user.getAttribute( EMAIL ) );
486
487                // Get created/modified timestamps
488                final String created = user.getAttribute( CREATED );
489                final String modified = user.getAttribute( LAST_MODIFIED );
490                profile.setCreated( parseDate( profile, created ) );
491                profile.setLastModified( parseDate( profile, modified ) );
492
493                // Is the profile locked?
494                final String lockExpiry = user.getAttribute( LOCK_EXPIRY );
495                if( lockExpiry == null || lockExpiry.length() == 0 ) {
496                    profile.setLockExpiry( null );
497                } else {
498                    profile.setLockExpiry( new Date( Long.parseLong( lockExpiry ) ) );
499                }
500
501                // Extract all of the user's attributes (should only be one attributes tag, but you never know!)
502                final NodeList attributes = user.getElementsByTagName( ATTRIBUTES_TAG );
503                for( int j = 0; j < attributes.getLength(); j++ ) {
504                    final Element attribute = ( Element )attributes.item( j );
505                    final String serializedMap = extractText( attribute );
506                    try {
507                        final Map< String, ? extends Serializable > map = Serializer.deserializeFromBase64( serializedMap );
508                        profile.getAttributes().putAll( map );
509                    } catch( final IOException e ) {
510                        log.error( "Could not parse user profile attributes!", e );
511                    }
512                }
513
514                return profile;
515            }
516        }
517        return null;
518    }
519
520    /**
521     * Extracts all of the text nodes that are immediate children of an Element.
522     *
523     * @param element the base element
524     * @return the text nodes that are immediate children of the base element, concatenated together
525     */
526    private String extractText( final Element element ) {
527        String text = "";
528        if( element.getChildNodes().getLength() > 0 ) {
529            final NodeList children = element.getChildNodes();
530            for( int k = 0; k < children.getLength(); k++ ) {
531                final Node child = children.item( k );
532                if( child.getNodeType() == Node.TEXT_NODE ) {
533                    text = text + ( ( Text )child ).getData();
534                }
535            }
536        }
537        return text;
538    }
539
540    /**
541     *  Tries to parse a date using the default format - then, for backwards compatibility reasons, tries the platform default.
542     *
543     *  @param profile
544     *  @param date
545     *  @return A parsed date, or null, if both parse attempts fail.
546     */
547    private Date parseDate( final UserProfile profile, final String date ) {
548        try {
549            final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
550            return c_format.parse( date );
551        } catch( final ParseException e ) {
552            try {
553                return DateFormat.getDateTimeInstance().parse( date );
554            } catch( final ParseException e2 ) {
555                log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + profile.getLoginName() + "'." +
556                          " It may have been tampered with.", e2 );
557            }
558        }
559        return null;
560    }
561
562    /**
563     * After loading the DOM, this method sanity-checks the dates in the DOM and makes sure they are formatted properly. This is sort-of
564     * hacky, but it should work.
565     */
566    private void sanitizeDOM() {
567        if( c_dom == null ) {
568            throw new IllegalStateException( "FATAL: database does not exist" );
569        }
570
571        final NodeList users = c_dom.getElementsByTagName( USER_TAG );
572        for( int i = 0; i < users.getLength(); i++ ) {
573            final Element user = ( Element )users.item( i );
574
575            // Sanitize UID (and generate a new one if one does not exist)
576            String uid = user.getAttribute( UID ).trim();
577            if( uid == null || uid.length() == 0 || "-1".equals( uid ) ) {
578                uid = String.valueOf( generateUid( this ) );
579                user.setAttribute( UID, uid );
580            }
581
582            // Sanitize dates
583            final String loginName = user.getAttribute( LOGIN_NAME );
584            String created = user.getAttribute( CREATED );
585            String modified = user.getAttribute( LAST_MODIFIED );
586            final DateFormat c_format = new SimpleDateFormat( DATE_FORMAT );
587            try {
588                created = c_format.format( c_format.parse( created ) );
589                modified = c_format.format( c_format.parse( modified ) );
590                user.setAttribute( CREATED, created );
591                user.setAttribute( LAST_MODIFIED, modified );
592            } catch( final ParseException e ) {
593                try {
594                    created = c_format.format( DateFormat.getDateTimeInstance().parse( created ) );
595                    modified = c_format.format( DateFormat.getDateTimeInstance().parse( modified ) );
596                    user.setAttribute( CREATED, created );
597                    user.setAttribute( LAST_MODIFIED, modified );
598                } catch( final ParseException e2 ) {
599                    log.warn( "Could not parse 'created' or 'lastModified' attribute for profile '" + loginName + "'."
600                            + " It may have been tampered with." );
601                }
602            }
603        }
604    }
605
606    /**
607     * Private method that sets an attribute value for a supplied DOM element.
608     *
609     * @param element the element whose attribute is to be set
610     * @param attribute the name of the attribute to set
611     * @param value the desired attribute value
612     */
613    private void setAttribute( final Element element, final String attribute, final String value ) {
614        if( value != null ) {
615            element.setAttribute( attribute, value );
616        }
617    }
618
619}