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