001/*
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.
018 */
019package org.apache.wiki.auth.user;
020
021import org.apache.commons.lang3.StringUtils;
022import org.apache.wiki.api.core.Engine;
023import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
024import org.apache.wiki.auth.NoSuchPrincipalException;
025import org.apache.wiki.auth.WikiPrincipal;
026import org.apache.wiki.auth.WikiSecurityException;
027import org.apache.wiki.util.Serializer;
028import org.apache.wiki.util.TextUtil;
029import org.w3c.dom.Document;
030import org.w3c.dom.Element;
031import org.w3c.dom.Node;
032import org.w3c.dom.NodeList;
033import org.w3c.dom.Text;
034import org.xml.sax.SAXException;
035
036import javax.xml.parsers.DocumentBuilderFactory;
037import javax.xml.parsers.ParserConfigurationException;
038import java.io.BufferedWriter;
039import java.io.File;
040import java.io.FileNotFoundException;
041import java.io.IOException;
042import java.io.OutputStreamWriter;
043import java.io.Serializable;
044import java.nio.charset.StandardCharsets;
045import java.nio.file.Files;
046import java.security.Principal;
047import java.text.DateFormat;
048import java.text.ParseException;
049import java.text.SimpleDateFormat;
050import java.util.Date;
051import java.util.Map;
052import java.util.Properties;
053import java.util.SortedSet;
054import java.util.TreeSet;
055
056/**
057 * <p>Manages {@link DefaultUserProfile} objects using XML files for persistence. Passwords are hashed using SHA1. User entries are simple
058 * <code>&lt;user&gt;</code> elements under the root. User profile properties are attributes of the element. For example:</p>
059 * <blockquote><code>
060 * &lt;users&gt;<br/>
061 * &nbsp;&nbsp;&lt;user loginName="janne" fullName="Janne Jalkanen"<br/>
062 * &nbsp;&nbsp;&nbsp;&nbsp;wikiName="JanneJalkanen" email="janne@ecyrd.com"<br/>
063 * &nbsp;&nbsp;&nbsp;&nbsp;password="{SHA}457b08e825da547c3b77fbc1ff906a1d00a7daee"/&gt;<br/>
064 * &lt;/users&gt;
065 * </code></blockquote>
066 * <p>In this example, the un-hashed password is <code>myP@5sw0rd</code>. Passwords are hashed without salt.</p>
067 * @since 2.3
068 */
069
070// FIXME: If the DB is shared across multiple systems, it's possible to lose accounts
071//        if two people add new accounts right after each other from different wikis.
072public class XMLUserDatabase extends AbstractUserDatabase {
073
074    /** The jspwiki.properties property specifying the file system location of the user database. */
075    public static final String  PROP_USERDATABASE = "jspwiki.xmlUserDatabaseFile";
076    private static final String DEFAULT_USERDATABASE = "userdatabase.xml";
077    private static final String ATTRIBUTES_TAG    = "attributes";
078    private static final String CREATED           = "created";
079    private static final String EMAIL             = "email";
080    private static final String FULL_NAME         = "fullName";
081    private static final String LOGIN_NAME        = "loginName";
082    private static final String LAST_MODIFIED     = "lastModified";
083    private static final String LOCK_EXPIRY       = "lockExpiry";
084    private static final String PASSWORD          = "password";
085    private static final String UID               = "uid";
086    private static final String USER_TAG          = "user";
087    private static final String WIKI_NAME         = "wikiName";
088    private static final String DATE_FORMAT       = "yyyy.MM.dd 'at' HH:mm:ss:SSS z";
089    private Document            c_dom;
090    private File                c_file;
091
092    /** {@inheritDoc} */
093    @Override
094    public synchronized void deleteByLoginName( final String loginName ) throws 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 &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 = !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}