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     */
019    package org.apache.wiki;
020    
021    import java.io.IOException;
022    import java.security.Permission;
023    import java.security.Principal;
024    import java.util.ArrayList;
025    import java.util.Collection;
026    import java.util.Date;
027    import java.util.Enumeration;
028    import java.util.HashMap;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Properties;
032    
033    import org.apache.commons.lang.ArrayUtils;
034    import org.apache.log4j.Logger;
035    import org.apache.wiki.api.engine.FilterManager;
036    import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
037    import org.apache.wiki.api.exceptions.ProviderException;
038    import org.apache.wiki.api.exceptions.WikiException;
039    import org.apache.wiki.auth.WikiPrincipal;
040    import org.apache.wiki.auth.WikiSecurityException;
041    import org.apache.wiki.auth.acl.Acl;
042    import org.apache.wiki.auth.acl.AclEntry;
043    import org.apache.wiki.auth.acl.AclEntryImpl;
044    import org.apache.wiki.auth.user.UserProfile;
045    import org.apache.wiki.event.WikiEvent;
046    import org.apache.wiki.event.WikiEventListener;
047    import org.apache.wiki.event.WikiEventManager;
048    import org.apache.wiki.event.WikiPageEvent;
049    import org.apache.wiki.event.WikiSecurityEvent;
050    import org.apache.wiki.modules.ModuleManager;
051    import org.apache.wiki.providers.RepositoryModifiedException;
052    import org.apache.wiki.providers.WikiPageProvider;
053    import org.apache.wiki.util.ClassUtil;
054    import org.apache.wiki.util.TextUtil;
055    import org.apache.wiki.workflow.Outcome;
056    import org.apache.wiki.workflow.Task;
057    import org.apache.wiki.workflow.Workflow;
058    
059    
060    /**
061     * Manages the WikiPages.  This class functions as an unified interface towards
062     * the page providers.  It handles initialization and management of the providers,
063     * and provides utility methods for accessing the contents.
064     * <p/>
065     * Saving a page is a two-stage Task; first the pre-save operations and then the
066     * actual save.  See the descriptions of the tasks for further information.
067     *
068     * @since 2.0
069     */
070    // FIXME: This class currently only functions just as an extra layer over providers,
071    //        complicating things.  We need to move more provider-specific functionality
072    //        from WikiEngine (which is too big now) into this class.
073    public class PageManager extends ModuleManager implements WikiEventListener {
074    
075        private static final long serialVersionUID = 1L;
076    
077        /**
078         * The property value for setting the current page provider.  Value is {@value}.
079         */
080        public static final String PROP_PAGEPROVIDER = "jspwiki.pageProvider";
081    
082        /**
083         * The property value for setting the cache on/off.  Value is {@value}.
084         */
085        public static final String PROP_USECACHE = "jspwiki.usePageCache";
086    
087        /**
088         * The property value for setting the amount of time before the page locks expire. Value is {@value}.
089         */
090        public static final String PROP_LOCKEXPIRY = "jspwiki.lockExpiryTime";
091    
092        /**
093         * The message key for storing the text for the presave task.  Value is <tt>{@value}</tt>
094         */
095        public static final String PRESAVE_TASK_MESSAGE_KEY = "task.preSaveWikiPage";
096    
097        /**
098         * The workflow attribute which stores the wikiContext.
099         */
100        public static final String PRESAVE_WIKI_CONTEXT = "wikiContext";
101    
102        /**
103         * The name of the key from jspwiki.properties which defines who shall approve
104         * the workflow of storing a wikipage.  Value is <tt>{@value}</tt>
105         */
106        public static final String SAVE_APPROVER = "workflow.saveWikiPage";
107    
108        /**
109         * The message key for storing the Decision text for saving a page.  Value is {@value}.
110         */
111        public static final String SAVE_DECISION_MESSAGE_KEY = "decision.saveWikiPage";
112    
113        /**
114         * The message key for rejecting the decision to save the page.  Value is {@value}.
115         */
116        public static final String SAVE_REJECT_MESSAGE_KEY = "notification.saveWikiPage.reject";
117    
118        /**
119         * The message key of the text to finally approve a page save.  Value is {@value}.
120         */
121        public static final String SAVE_TASK_MESSAGE_KEY = "task.saveWikiPage";
122    
123        /**
124         * Fact name for storing the page name.  Value is {@value}.
125         */
126        public static final String FACT_PAGE_NAME = "fact.pageName";
127    
128        /**
129         * Fact name for storing a diff text. Value is {@value}.
130         */
131        public static final String FACT_DIFF_TEXT = "fact.diffText";
132    
133        /**
134         * Fact name for storing the current text.  Value is {@value}.
135         */
136        public static final String FACT_CURRENT_TEXT = "fact.currentText";
137    
138        /**
139         * Fact name for storing the proposed (edited) text.  Value is {@value}.
140         */
141        public static final String FACT_PROPOSED_TEXT = "fact.proposedText";
142    
143        /**
144         * Fact name for storing whether the user is authenticated or not.  Value is {@value}.
145         */
146        public static final String FACT_IS_AUTHENTICATED = "fact.isAuthenticated";
147    
148        static Logger log = Logger.getLogger(PageManager.class);
149    
150        private WikiPageProvider m_provider;
151    
152        protected HashMap<String, PageLock> m_pageLocks = new HashMap<String, PageLock>();
153    
154        private WikiEngine m_engine;
155    
156        private int m_expiryTime = 60;
157    
158        private LockReaper m_reaper = null;
159    
160        /**
161         * Creates a new PageManager.
162         *
163         * @param engine WikiEngine instance
164         * @param props  Properties to use for initialization
165         * @throws WikiException If anything goes wrong, you get this.
166         */
167        public PageManager(WikiEngine engine, Properties props) throws WikiException {
168            super(engine);
169            String classname;
170            m_engine = engine;
171            boolean useCache = "true".equals(props.getProperty(PROP_USECACHE));
172    
173            m_expiryTime = TextUtil.parseIntParameter(props.getProperty(PROP_LOCKEXPIRY), 60);
174    
175            //
176            //  If user wants to use a cache, then we'll use the CachingProvider.
177            //
178            if (useCache) {
179                classname = "org.apache.wiki.providers.CachingProvider";
180            } else {
181                classname = TextUtil.getRequiredProperty(props, PROP_PAGEPROVIDER);
182            }
183    
184            try {
185                log.debug("Page provider class: '" + classname + "'");
186                Class<?> providerclass = ClassUtil.findClass("org.apache.wiki.providers", classname);
187                m_provider = (WikiPageProvider) providerclass.newInstance();
188    
189                log.debug("Initializing page provider class " + m_provider);
190                m_provider.initialize(m_engine, props);
191            } catch (ClassNotFoundException e) {
192                log.error("Unable to locate provider class '" + classname + "'", e);
193                throw new WikiException("No provider class.", e);
194            } catch (InstantiationException e) {
195                log.error("Unable to create provider class '" + classname + "'", e);
196                throw new WikiException("Faulty provider class.", e);
197            } catch (IllegalAccessException e) {
198                log.error("Illegal access to provider class '" + classname + "'", e);
199                throw new WikiException("Illegal provider class.", e);
200            } catch (NoRequiredPropertyException e) {
201                log.error("Provider did not found a property it was looking for: " + e.getMessage(), e);
202                throw e;  // Same exception works.
203            } catch (IOException e) {
204                log.error("An I/O exception occurred while trying to create a new page provider: " + classname, e);
205                throw new WikiException("Unable to start page provider: " + e.getMessage(), e);
206            }
207    
208        }
209    
210        /**
211         * Returns the page provider currently in use.
212         *
213         * @return A WikiPageProvider instance.
214         */
215        public WikiPageProvider getProvider() {
216            return m_provider;
217        }
218    
219        /**
220         * Returns all pages in some random order.  If you need just the page names,
221         * please see {@link ReferenceManager#findCreated()}, which is probably a lot
222         * faster.  This method may cause repository access.
223         *
224         * @return A Collection of WikiPage objects.
225         * @throws ProviderException If the backend has problems.
226         */
227        public Collection getAllPages() throws ProviderException {
228            return m_provider.getAllPages();
229        }
230    
231        /**
232         * Fetches the page text from the repository.  This method also does some sanity checks,
233         * like checking for the pageName validity, etc.  Also, if the page repository has been
234         * modified externally, it is smart enough to handle such occurrences.
235         *
236         * @param pageName The name of the page to fetch.
237         * @param version  The version to find
238         * @return The page content as a raw string
239         * @throws ProviderException If the backend has issues.
240         */
241        public String getPageText(String pageName, int version) throws ProviderException {
242            if (pageName == null || pageName.length() == 0) {
243                throw new ProviderException("Illegal page name");
244            }
245            String text = null;
246    
247            try {
248                text = m_provider.getPageText(pageName, version);
249            } catch (RepositoryModifiedException e) {
250                //
251                //  This only occurs with the latest version.
252                //
253                log.info("Repository has been modified externally while fetching page " + pageName);
254    
255                //
256                //  Empty the references and yay, it shall be recalculated
257                //
258                //WikiPage p = new WikiPage( pageName );
259                WikiPage p = m_provider.getPageInfo(pageName, version);
260    
261                m_engine.updateReferences(p);
262    
263                if (p != null) {
264                    m_engine.getSearchManager().reindexPage(p);
265                    text = m_provider.getPageText(pageName, version);
266                } else {
267                    //
268                    //  Make sure that it no longer exists in internal data structures either.
269                    //
270                    WikiPage dummy = new WikiPage(m_engine, pageName);
271                    m_engine.getSearchManager().pageRemoved(dummy);
272                    m_engine.getReferenceManager().pageRemoved(dummy);
273                }
274            }
275    
276            return text;
277        }
278    
279        /**
280         * Returns the WikiEngine to which this PageManager belongs to.
281         *
282         * @return The WikiEngine object.
283         */
284        public WikiEngine getEngine() {
285            return m_engine;
286        }
287    
288        /**
289         * Puts the page text into the repository.  Note that this method does NOT update
290         * JSPWiki internal data structures, and therefore you should always use WikiEngine.saveText()
291         *
292         * @param page    Page to save
293         * @param content Wikimarkup to save
294         * @throws ProviderException If something goes wrong in the saving phase
295         */
296        public void putPageText(WikiPage page, String content) throws ProviderException {
297            if (page == null || page.getName() == null || page.getName().length() == 0) {
298                throw new ProviderException("Illegal page name");
299            }
300    
301            m_provider.putPageText(page, content);
302        }
303    
304        /**
305         * Locks page for editing.  Note, however, that the PageManager
306         * will in no way prevent you from actually editing this page;
307         * the lock is just for information.
308         *
309         * @param page WikiPage to lock
310         * @param user Username to use for locking
311         * @return null, if page could not be locked.
312         */
313        public PageLock lockPage(WikiPage page, String user) {
314            PageLock lock = null;
315    
316            if (m_reaper == null) {
317                //
318                //  Start the lock reaper lazily.  We don't want to start it in
319                //  the constructor, because starting threads in constructors
320                //  is a bad idea when it comes to inheritance.  Besides,
321                //  laziness is a virtue.
322                //
323                m_reaper = new LockReaper(m_engine);
324                m_reaper.start();
325            }
326    
327            synchronized (m_pageLocks) {
328                fireEvent(WikiPageEvent.PAGE_LOCK, page.getName()); // prior to or after actual lock?
329                lock = m_pageLocks.get(page.getName());
330    
331                if (lock == null) {
332                    //
333                    //  Lock is available, so make a lock.
334                    //
335                    Date d = new Date();
336                    lock = new PageLock(page, user, d, new Date(d.getTime() + m_expiryTime * 60 * 1000L));
337                    m_pageLocks.put(page.getName(), lock);
338                    log.debug("Locked page " + page.getName() + " for " + user);
339                } else {
340                    log.debug("Page " + page.getName() + " already locked by " + lock.getLocker());
341                    lock = null; // Nothing to return
342                }
343            }
344    
345            return lock;
346        }
347    
348        /**
349         * Marks a page free to be written again.  If there has not been a lock, will fail quietly.
350         *
351         * @param lock A lock acquired in lockPage().  Safe to be null.
352         */
353        public void unlockPage(PageLock lock) {
354            if (lock == null) {
355                return;
356            }
357    
358            synchronized (m_pageLocks) {
359                m_pageLocks.remove(lock.getPage());
360                log.debug("Unlocked page " + lock.getPage());
361            }
362    
363            fireEvent(WikiPageEvent.PAGE_UNLOCK, lock.getPage());
364        }
365    
366        /**
367         * Returns the current lock owner of a page.  If the page is not
368         * locked, will return null.
369         *
370         * @param page The page to check the lock for
371         * @return Current lock, or null, if there is no lock
372         */
373        public PageLock getCurrentLock(WikiPage page) {
374            PageLock lock = null;
375    
376            synchronized (m_pageLocks) {
377                lock = m_pageLocks.get(page.getName());
378            }
379    
380            return lock;
381        }
382    
383        /**
384         * Returns a list of currently applicable locks.  Note that by the time you get the list,
385         * the locks may have already expired, so use this only for informational purposes.
386         *
387         * @return List of PageLock objects, detailing the locks.  If no locks exist, returns
388         *         an empty list.
389         * @since 2.0.22.
390         */
391        public List<PageLock> getActiveLocks() {
392            ArrayList<PageLock> result = new ArrayList<PageLock>();
393    
394            synchronized (m_pageLocks) {
395                for (PageLock lock : m_pageLocks.values()) {
396                    result.add(lock);
397                }
398            }
399    
400            return result;
401        }
402    
403        /**
404         * Finds a WikiPage object describing a particular page and version.
405         *
406         * @param pageName The name of the page
407         * @param version  A version number
408         * @return A WikiPage object, or null, if the page does not exist
409         * @throws ProviderException If there is something wrong with the page
410         *                           name or the repository
411         */
412        public WikiPage getPageInfo(String pageName, int version) throws ProviderException {
413            if (pageName == null || pageName.length() == 0) {
414                throw new ProviderException("Illegal page name '" + pageName + "'");
415            }
416    
417            WikiPage page = null;
418    
419            try {
420                page = m_provider.getPageInfo(pageName, version);
421            } catch (RepositoryModifiedException e) {
422                //
423                //  This only occurs with the latest version.
424                //
425                log.info("Repository has been modified externally while fetching info for " + pageName);
426                page = m_provider.getPageInfo(pageName, version);
427                if (page != null) {
428                    m_engine.updateReferences(page);
429                } else {
430                    m_engine.getReferenceManager().pageRemoved(new WikiPage(m_engine, pageName));
431                }
432            }
433    
434            //
435            //  Should update the metadata.
436            //
437            /*
438            if( page != null && !page.hasMetadata() )
439            {
440                WikiContext ctx = new WikiContext(m_engine,page);
441                m_engine.textToHTML( ctx, getPageText(pageName,version) );
442            }
443            */
444            return page;
445        }
446    
447        /**
448         * Gets a version history of page.  Each element in the returned
449         * List is a WikiPage.
450         *
451         * @param pageName The name of the page to fetch history for
452         * @return If the page does not exist, returns null, otherwise a List
453         *         of WikiPages.
454         * @throws ProviderException If the repository fails.
455         */
456        public List getVersionHistory(String pageName) throws ProviderException {
457            if (pageExists(pageName)) {
458                return m_provider.getVersionHistory(pageName);
459            }
460    
461            return null;
462        }
463    
464        /**
465         * Returns a human-readable description of the current provider.
466         *
467         * @return A human-readable description.
468         */
469        public String getProviderDescription() {
470            return m_provider.getProviderInfo();
471        }
472    
473        /**
474         * Returns the total count of all pages in the repository. This
475         * method is equivalent of calling getAllPages().size(), but
476         * it swallows the ProviderException and returns -1 instead of
477         * any problems.
478         *
479         * @return The number of pages, or -1, if there is an error.
480         */
481        public int getTotalPageCount() {
482            try {
483                return m_provider.getAllPages().size();
484            } catch (ProviderException e) {
485                log.error("Unable to count pages: ", e);
486                return -1;
487            }
488        }
489    
490        /**
491         * Returns true, if the page exists (any version).
492         *
493         * @param pageName Name of the page.
494         * @return A boolean value describing the existence of a page
495         * @throws ProviderException If the backend fails or the name is illegal.
496         */
497        public boolean pageExists(String pageName) throws ProviderException {
498            if (pageName == null || pageName.length() == 0) {
499                throw new ProviderException("Illegal page name");
500            }
501    
502            return m_provider.pageExists(pageName);
503        }
504    
505        /**
506         * Checks for existence of a specific page and version.
507         *
508         * @param pageName Name of the page
509         * @param version  The version to check
510         * @return <code>true</code> if the page exists, <code>false</code> otherwise
511         * @throws ProviderException If backend fails or name is illegal
512         * @since 2.3.29
513         */
514        public boolean pageExists(String pageName, int version) throws ProviderException {
515            if (pageName == null || pageName.length() == 0) {
516                throw new ProviderException("Illegal page name");
517            }
518    
519            if (version == WikiProvider.LATEST_VERSION) {
520                return pageExists(pageName);
521            }
522    
523            return m_provider.pageExists(pageName, version);
524        }
525    
526        /**
527         * Deletes only a specific version of a WikiPage.
528         *
529         * @param page The page to delete.
530         * @throws ProviderException if the page fails
531         */
532        public void deleteVersion(WikiPage page) throws ProviderException {
533            m_provider.deleteVersion(page.getName(), page.getVersion());
534    
535            // FIXME: If this was the latest, reindex Lucene
536            // FIXME: Update RefMgr
537        }
538    
539        /**
540         * Deletes an entire page, all versions, all traces.
541         *
542         * @param page The WikiPage to delete
543         * @throws ProviderException If the repository operation fails
544         */
545        public void deletePage(WikiPage page) throws ProviderException {
546            fireEvent(WikiPageEvent.PAGE_DELETE_REQUEST, page.getName());
547            m_provider.deletePage(page.getName());
548            fireEvent(WikiPageEvent.PAGE_DELETED, page.getName());
549        }
550    
551        /**
552         * This is a simple reaper thread that runs roughly every minute
553         * or so (it's not really that important, as long as it runs),
554         * and removes all locks that have expired.
555         */
556        private class LockReaper extends WikiBackgroundThread {
557            /**
558             * Create a LockReaper for a given engine.
559             *
560             * @param engine WikiEngine to own this thread.
561             */
562            public LockReaper(WikiEngine engine) {
563                super(engine, 60);
564                setName("JSPWiki Lock Reaper");
565            }
566    
567            public void backgroundTask() throws Exception {
568                synchronized (m_pageLocks) {
569                    Collection<PageLock> entries = m_pageLocks.values();
570                    Date now = new Date();
571                    for (Iterator<PageLock> i = entries.iterator(); i.hasNext(); ) {
572                        PageLock p = i.next();
573    
574                        if (now.after(p.getExpiryTime())) {
575                            i.remove();
576    
577                            log.debug("Reaped lock: " + p.getPage() +
578                                    " by " + p.getLocker() +
579                                    ", acquired " + p.getAcquisitionTime() +
580                                    ", and expired " + p.getExpiryTime());
581                        }
582                    }
583                }
584            }
585        }
586    
587        // workflow task inner classes....................................................
588    
589        /**
590         * Inner class that handles the page pre-save actions. If the proposed page
591         * text is the same as the current version, the {@link #execute()} method
592         * returns {@link org.apache.wiki.workflow.Outcome#STEP_ABORT}. Any
593         * WikiExceptions thrown by page filters will be re-thrown, and the workflow
594         * will abort.
595         */
596        public static class PreSaveWikiPageTask extends Task {
597            private static final long serialVersionUID = 6304715570092804615L;
598            private final WikiContext m_context;
599            private final String m_proposedText;
600    
601            /**
602             * Creates the task.
603             *
604             * @param context      The WikiContext
605             * @param proposedText The text that was just saved.
606             */
607            public PreSaveWikiPageTask(WikiContext context, String proposedText) {
608                super(PRESAVE_TASK_MESSAGE_KEY);
609                m_context = context;
610                m_proposedText = proposedText;
611            }
612    
613            /**
614             * {@inheritDoc}
615             */
616            @Override
617            public Outcome execute() throws WikiException {
618                // Retrieve attributes
619                WikiEngine engine = m_context.getEngine();
620                Workflow workflow = getWorkflow();
621    
622                // Get the wiki page
623                WikiPage page = m_context.getPage();
624    
625                // Figure out who the author was. Prefer the author
626                // set programmatically; otherwise get from the
627                // current logged in user
628                if (page.getAuthor() == null) {
629                    Principal wup = m_context.getCurrentUser();
630    
631                    if (wup != null) {
632                        page.setAuthor(wup.getName());
633                    }
634                }
635    
636                // Run the pre-save filters. If any exceptions, add error to list, abort, and redirect
637                String saveText;
638                FilterManager fm = engine.getFilterManager();
639                saveText = fm.doPreSaveFiltering(m_context, m_proposedText);
640    
641                // Stash the wiki context, old and new text as workflow attributes
642                workflow.setAttribute(PRESAVE_WIKI_CONTEXT, m_context);
643                workflow.setAttribute(FACT_PROPOSED_TEXT, saveText);
644                return Outcome.STEP_COMPLETE;
645            }
646        }
647    
648        /**
649         * Inner class that handles the actual page save and post-save actions. Instances
650         * of this class are assumed to have been added to an approval workflow via
651         * {@link org.apache.wiki.workflow.WorkflowBuilder#buildApprovalWorkflow(Principal, String, Task, String, org.apache.wiki.workflow.Fact[], Task, String)};
652         * they will not function correctly otherwise.
653         */
654        public static class SaveWikiPageTask extends Task {
655            private static final long serialVersionUID = 3190559953484411420L;
656    
657            /**
658             * Creates the Task.
659             */
660            public SaveWikiPageTask() {
661                super(SAVE_TASK_MESSAGE_KEY);
662            }
663    
664            /**
665             * {@inheritDoc}
666             */
667            @Override
668            public Outcome execute() throws WikiException {
669                // Retrieve attributes
670                WikiContext context = (WikiContext) getWorkflow().getAttribute(PRESAVE_WIKI_CONTEXT);
671                String proposedText = (String) getWorkflow().getAttribute(FACT_PROPOSED_TEXT);
672    
673                WikiEngine engine = context.getEngine();
674                WikiPage page = context.getPage();
675    
676                // Let the rest of the engine handle actual saving.
677                engine.getPageManager().putPageText(page, proposedText);
678    
679                // Refresh the context for post save filtering.
680                engine.getPage(page.getName());
681                engine.textToHTML(context, proposedText);
682                FilterManager fm = engine.getFilterManager();
683                fm.doPostSaveFiltering(context, proposedText);
684    
685                return Outcome.STEP_COMPLETE;
686            }
687        }
688    
689        // events processing .......................................................
690    
691        /**
692         * Fires a WikiPageEvent of the provided type and page name
693         * to all registered listeners.
694         *
695         * @param type     the event type to be fired
696         * @param pagename the wiki page name as a String
697         * @see org.apache.wiki.event.WikiPageEvent
698         */
699        protected final void fireEvent(int type, String pagename) {
700            if (WikiEventManager.isListening(this)) {
701                WikiEventManager.fireEvent(this, new WikiPageEvent(m_engine, type, pagename));
702            }
703        }
704    
705        /**
706         * {@inheritDoc}
707         */
708        @Override
709        public Collection modules() {
710            // TODO Auto-generated method stub
711            return null;
712        }
713    
714    
715        /**
716         * Listens for {@link org.apache.wiki.event.WikiSecurityEvent#PROFILE_NAME_CHANGED}
717         * events. If a user profile's name changes, each page ACL is inspected. If an entry contains
718         * a name that has changed, it is replaced with the new one. No events are emitted
719         * as a consequence of this method, because the page contents are still the same; it is
720         * only the representations of the names within the ACL that are changing.
721         *
722         * @param event The event
723         */
724        public void actionPerformed(WikiEvent event) {
725            if (!(event instanceof WikiSecurityEvent)) {
726                return;
727            }
728    
729            WikiSecurityEvent se = (WikiSecurityEvent) event;
730            if (se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED) {
731                UserProfile[] profiles = (UserProfile[]) se.getTarget();
732                Principal[] oldPrincipals = new Principal[]
733                        {new WikiPrincipal(profiles[0].getLoginName()),
734                                new WikiPrincipal(profiles[0].getFullname()),
735                                new WikiPrincipal(profiles[0].getWikiName())};
736                Principal newPrincipal = new WikiPrincipal(profiles[1].getFullname());
737    
738                // Examine each page ACL
739                try {
740                    int pagesChanged = 0;
741                    Collection pages = getAllPages();
742                    for (Iterator it = pages.iterator(); it.hasNext(); ) {
743                        WikiPage page = (WikiPage) it.next();
744                        boolean aclChanged = changeAcl(page, oldPrincipals, newPrincipal);
745                        if (aclChanged) {
746                            // If the Acl needed changing, change it now
747                            try {
748                                m_engine.getAclManager().setPermissions(page, page.getAcl());
749                            } catch (WikiSecurityException e) {
750                                log.error("Could not change page ACL for page " + page.getName() + ": " + e.getMessage(), e);
751                            }
752                            pagesChanged++;
753                        }
754                    }
755                    log.info("Profile name change for '" + newPrincipal.toString() +
756                            "' caused " + pagesChanged + " page ACLs to change also.");
757                } catch (ProviderException e) {
758                    // Oooo! This is really bad...
759                    log.error("Could not change user name in Page ACLs because of Provider error:" + e.getMessage(), e);
760                }
761            }
762        }
763    
764        /**
765         * For a single wiki page, replaces all Acl entries matching a supplied array of Principals
766         * with a new Principal.
767         *
768         * @param page          the wiki page whose Acl is to be modified
769         * @param oldPrincipals an array of Principals to replace; all AclEntry objects whose
770         *                      {@link AclEntry#getPrincipal()} method returns one of these Principals will be replaced
771         * @param newPrincipal  the Principal that should receive the old Principals' permissions
772         * @return <code>true</code> if the Acl was actually changed; <code>false</code> otherwise
773         */
774        protected boolean changeAcl(WikiPage page, Principal[] oldPrincipals, Principal newPrincipal) {
775            Acl acl = page.getAcl();
776            boolean pageChanged = false;
777            if (acl != null) {
778                Enumeration<AclEntry> entries = acl.entries();
779                Collection<AclEntry> entriesToAdd = new ArrayList<AclEntry>();
780                Collection<AclEntry> entriesToRemove = new ArrayList<AclEntry>();
781                while (entries.hasMoreElements()) {
782                    AclEntry entry = entries.nextElement();
783                    if (ArrayUtils.contains(oldPrincipals, entry.getPrincipal())) {
784                        // Create new entry
785                        AclEntry newEntry = new AclEntryImpl();
786                        newEntry.setPrincipal(newPrincipal);
787                        Enumeration<Permission> permissions = entry.permissions();
788                        while (permissions.hasMoreElements()) {
789                            Permission permission = permissions.nextElement();
790                            newEntry.addPermission(permission);
791                        }
792                        pageChanged = true;
793                        entriesToRemove.add(entry);
794                        entriesToAdd.add(newEntry);
795                    }
796                }
797                for (Iterator<AclEntry> ix = entriesToRemove.iterator(); ix.hasNext(); ) {
798                    AclEntry entry = ix.next();
799                    acl.removeEntry(entry);
800                }
801                for (Iterator<AclEntry> ix = entriesToAdd.iterator(); ix.hasNext(); ) {
802                    AclEntry entry = ix.next();
803                    acl.addEntry(entry);
804                }
805            }
806            return pageChanged;
807        }
808    
809    }