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.Iterator;
029import java.util.List;
030import java.util.Properties;
031import java.util.concurrent.ConcurrentHashMap;
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 ConcurrentHashMap<String, PageLock> m_pageLocks = new ConcurrentHashMap<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        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        return lock;
345    }
346
347    /**
348     * Marks a page free to be written again.  If there has not been a lock, will fail quietly.
349     *
350     * @param lock A lock acquired in lockPage().  Safe to be null.
351     */
352    public void unlockPage(PageLock lock) {
353        if (lock == null) {
354            return;
355        }
356
357        m_pageLocks.remove(lock.getPage());
358        log.debug("Unlocked page " + lock.getPage());
359
360        fireEvent(WikiPageEvent.PAGE_UNLOCK, lock.getPage());
361    }
362
363    /**
364     * Returns the current lock owner of a page.  If the page is not
365     * locked, will return null.
366     *
367     * @param page The page to check the lock for
368     * @return Current lock, or null, if there is no lock
369     */
370    public PageLock getCurrentLock(WikiPage page) {
371        return m_pageLocks.get(page.getName());
372    }
373
374    /**
375     * Returns a list of currently applicable locks.  Note that by the time you get the list,
376     * the locks may have already expired, so use this only for informational purposes.
377     *
378     * @return List of PageLock objects, detailing the locks.  If no locks exist, returns
379     *         an empty list.
380     * @since 2.0.22.
381     */
382    public List<PageLock> getActiveLocks() {
383        ArrayList<PageLock> result = new ArrayList<PageLock>();
384
385        for (PageLock lock : m_pageLocks.values()) {
386            result.add(lock);
387        }
388
389        return result;
390    }
391
392    /**
393     * Finds a WikiPage object describing a particular page and version.
394     *
395     * @param pageName The name of the page
396     * @param version  A version number
397     * @return A WikiPage object, or null, if the page does not exist
398     * @throws ProviderException If there is something wrong with the page
399     *                           name or the repository
400     */
401    public WikiPage getPageInfo(String pageName, int version) throws ProviderException {
402        if (pageName == null || pageName.length() == 0) {
403            throw new ProviderException("Illegal page name '" + pageName + "'");
404        }
405
406        WikiPage page = null;
407
408        try {
409            page = m_provider.getPageInfo(pageName, version);
410        } catch (RepositoryModifiedException e) {
411            //
412            //  This only occurs with the latest version.
413            //
414            log.info("Repository has been modified externally while fetching info for " + pageName);
415            page = m_provider.getPageInfo(pageName, version);
416            if (page != null) {
417                m_engine.updateReferences(page);
418            } else {
419                m_engine.getReferenceManager().pageRemoved(new WikiPage(m_engine, pageName));
420            }
421        }
422
423        //
424        //  Should update the metadata.
425        //
426        /*
427        if( page != null && !page.hasMetadata() )
428        {
429            WikiContext ctx = new WikiContext(m_engine,page);
430            m_engine.textToHTML( ctx, getPageText(pageName,version) );
431        }
432        */
433        return page;
434    }
435
436    /**
437     * Gets a version history of page.  Each element in the returned
438     * List is a WikiPage.
439     *
440     * @param pageName The name of the page to fetch history for
441     * @return If the page does not exist, returns null, otherwise a List
442     *         of WikiPages.
443     * @throws ProviderException If the repository fails.
444     */
445    public List getVersionHistory(String pageName) throws ProviderException {
446        if (pageExists(pageName)) {
447            return m_provider.getVersionHistory(pageName);
448        }
449
450        return null;
451    }
452
453    /**
454     * Returns a human-readable description of the current provider.
455     *
456     * @return A human-readable description.
457     */
458    public String getProviderDescription() {
459        return m_provider.getProviderInfo();
460    }
461
462    /**
463     * Returns the total count of all pages in the repository. This
464     * method is equivalent of calling getAllPages().size(), but
465     * it swallows the ProviderException and returns -1 instead of
466     * any problems.
467     *
468     * @return The number of pages, or -1, if there is an error.
469     */
470    public int getTotalPageCount() {
471        try {
472            return m_provider.getAllPages().size();
473        } catch (ProviderException e) {
474            log.error("Unable to count pages: ", e);
475            return -1;
476        }
477    }
478
479    /**
480     * Returns true, if the page exists (any version).
481     *
482     * @param pageName Name of the page.
483     * @return A boolean value describing the existence of a page
484     * @throws ProviderException If the backend fails or the name is illegal.
485     */
486    public boolean pageExists(String pageName) throws ProviderException {
487        if (pageName == null || pageName.length() == 0) {
488            throw new ProviderException("Illegal page name");
489        }
490
491        return m_provider.pageExists(pageName);
492    }
493
494    /**
495     * Checks for existence of a specific page and version.
496     *
497     * @param pageName Name of the page
498     * @param version  The version to check
499     * @return <code>true</code> if the page exists, <code>false</code> otherwise
500     * @throws ProviderException If backend fails or name is illegal
501     * @since 2.3.29
502     */
503    public boolean pageExists(String pageName, int version) throws ProviderException {
504        if (pageName == null || pageName.length() == 0) {
505            throw new ProviderException("Illegal page name");
506        }
507
508        if (version == WikiProvider.LATEST_VERSION) {
509            return pageExists(pageName);
510        }
511
512        return m_provider.pageExists(pageName, version);
513    }
514
515    /**
516     * Deletes only a specific version of a WikiPage.
517     *
518     * @param page The page to delete.
519     * @throws ProviderException if the page fails
520     */
521    public void deleteVersion(WikiPage page) throws ProviderException {
522        m_provider.deleteVersion(page.getName(), page.getVersion());
523
524        // FIXME: If this was the latest, reindex Lucene
525        // FIXME: Update RefMgr
526    }
527
528    /**
529     * Deletes an entire page, all versions, all traces.
530     *
531     * @param page The WikiPage to delete
532     * @throws ProviderException If the repository operation fails
533     */
534    public void deletePage(WikiPage page) throws ProviderException {
535        fireEvent(WikiPageEvent.PAGE_DELETE_REQUEST, page.getName());
536        m_provider.deletePage(page.getName());
537        fireEvent(WikiPageEvent.PAGE_DELETED, page.getName());
538    }
539
540    /**
541     * This is a simple reaper thread that runs roughly every minute
542     * or so (it's not really that important, as long as it runs),
543     * and removes all locks that have expired.
544     */
545    private class LockReaper extends WikiBackgroundThread {
546        /**
547         * Create a LockReaper for a given engine.
548         *
549         * @param engine WikiEngine to own this thread.
550         */
551        public LockReaper(WikiEngine engine) {
552            super(engine, 60);
553            setName("JSPWiki Lock Reaper");
554        }
555
556        public void backgroundTask() throws Exception {
557            Collection<PageLock> entries = m_pageLocks.values();
558            Date now = new Date();
559            for (Iterator<PageLock> i = entries.iterator(); i.hasNext(); ) {
560                PageLock p = i.next();
561
562                if (now.after(p.getExpiryTime())) {
563                    i.remove();
564
565                    log.debug("Reaped lock: " + p.getPage() +
566                              " by " + p.getLocker() +
567                              ", acquired " + p.getAcquisitionTime() +
568                              ", and expired " + p.getExpiryTime());
569                }
570            }
571        }
572    }
573
574    // workflow task inner classes....................................................
575
576    /**
577     * Inner class that handles the page pre-save actions. If the proposed page
578     * text is the same as the current version, the {@link #execute()} method
579     * returns {@link org.apache.wiki.workflow.Outcome#STEP_ABORT}. Any
580     * WikiExceptions thrown by page filters will be re-thrown, and the workflow
581     * will abort.
582     */
583    public static class PreSaveWikiPageTask extends Task {
584        private static final long serialVersionUID = 6304715570092804615L;
585        private final WikiContext m_context;
586        private final String m_proposedText;
587
588        /**
589         * Creates the task.
590         *
591         * @param context      The WikiContext
592         * @param proposedText The text that was just saved.
593         */
594        public PreSaveWikiPageTask(WikiContext context, String proposedText) {
595            super(PRESAVE_TASK_MESSAGE_KEY);
596            m_context = context;
597            m_proposedText = proposedText;
598        }
599
600        /**
601         * {@inheritDoc}
602         */
603        @Override
604        public Outcome execute() throws WikiException {
605            // Retrieve attributes
606            WikiEngine engine = m_context.getEngine();
607            Workflow workflow = getWorkflow();
608
609            // Get the wiki page
610            WikiPage page = m_context.getPage();
611
612            // Figure out who the author was. Prefer the author
613            // set programmatically; otherwise get from the
614            // current logged in user
615            if (page.getAuthor() == null) {
616                Principal wup = m_context.getCurrentUser();
617
618                if (wup != null) {
619                    page.setAuthor(wup.getName());
620                }
621            }
622
623            // Run the pre-save filters. If any exceptions, add error to list, abort, and redirect
624            String saveText;
625            FilterManager fm = engine.getFilterManager();
626            saveText = fm.doPreSaveFiltering(m_context, m_proposedText);
627
628            // Stash the wiki context, old and new text as workflow attributes
629            workflow.setAttribute(PRESAVE_WIKI_CONTEXT, m_context);
630            workflow.setAttribute(FACT_PROPOSED_TEXT, saveText);
631            return Outcome.STEP_COMPLETE;
632        }
633    }
634
635    /**
636     * Inner class that handles the actual page save and post-save actions. Instances
637     * of this class are assumed to have been added to an approval workflow via
638     * {@link org.apache.wiki.workflow.WorkflowBuilder#buildApprovalWorkflow(Principal, String, Task, String, org.apache.wiki.workflow.Fact[], Task, String)};
639     * they will not function correctly otherwise.
640     */
641    public static class SaveWikiPageTask extends Task {
642        private static final long serialVersionUID = 3190559953484411420L;
643
644        /**
645         * Creates the Task.
646         */
647        public SaveWikiPageTask() {
648            super(SAVE_TASK_MESSAGE_KEY);
649        }
650
651        /**
652         * {@inheritDoc}
653         */
654        @Override
655        public Outcome execute() throws WikiException {
656            // Retrieve attributes
657            WikiContext context = (WikiContext) getWorkflow().getAttribute(PRESAVE_WIKI_CONTEXT);
658            String proposedText = (String) getWorkflow().getAttribute(FACT_PROPOSED_TEXT);
659
660            WikiEngine engine = context.getEngine();
661            WikiPage page = context.getPage();
662
663            // Let the rest of the engine handle actual saving.
664            engine.getPageManager().putPageText(page, proposedText);
665
666            // Refresh the context for post save filtering.
667            engine.getPage(page.getName());
668            engine.textToHTML(context, proposedText);
669            FilterManager fm = engine.getFilterManager();
670            fm.doPostSaveFiltering(context, proposedText);
671
672            return Outcome.STEP_COMPLETE;
673        }
674    }
675
676    // events processing .......................................................
677
678    /**
679     * Fires a WikiPageEvent of the provided type and page name
680     * to all registered listeners.
681     *
682     * @param type     the event type to be fired
683     * @param pagename the wiki page name as a String
684     * @see org.apache.wiki.event.WikiPageEvent
685     */
686    protected final void fireEvent(int type, String pagename) {
687        if (WikiEventManager.isListening(this)) {
688            WikiEventManager.fireEvent(this, new WikiPageEvent(m_engine, type, pagename));
689        }
690    }
691
692    /**
693     * {@inheritDoc}
694     */
695    @Override
696    public Collection< WikiModuleInfo > modules() {
697        return null;
698    }
699
700    /**
701     * Returns null!
702     *  {@inheritDoc}
703     */
704    @Override
705    public WikiModuleInfo getModuleInfo(String moduleName) {
706        return null;
707    }
708
709    /**
710     * Listens for {@link org.apache.wiki.event.WikiSecurityEvent#PROFILE_NAME_CHANGED}
711     * events. If a user profile's name changes, each page ACL is inspected. If an entry contains
712     * a name that has changed, it is replaced with the new one. No events are emitted
713     * as a consequence of this method, because the page contents are still the same; it is
714     * only the representations of the names within the ACL that are changing.
715     *
716     * @param event The event
717     */
718    public void actionPerformed(WikiEvent event) {
719        if (!(event instanceof WikiSecurityEvent)) {
720            return;
721        }
722
723        WikiSecurityEvent se = (WikiSecurityEvent) event;
724        if (se.getType() == WikiSecurityEvent.PROFILE_NAME_CHANGED) {
725            UserProfile[] profiles = (UserProfile[]) se.getTarget();
726            Principal[] oldPrincipals = new Principal[]
727                    {new WikiPrincipal(profiles[0].getLoginName()),
728                            new WikiPrincipal(profiles[0].getFullname()),
729                            new WikiPrincipal(profiles[0].getWikiName())};
730            Principal newPrincipal = new WikiPrincipal(profiles[1].getFullname());
731
732            // Examine each page ACL
733            try {
734                int pagesChanged = 0;
735                Collection pages = getAllPages();
736                for (Iterator it = pages.iterator(); it.hasNext(); ) {
737                    WikiPage page = (WikiPage) it.next();
738                    boolean aclChanged = changeAcl(page, oldPrincipals, newPrincipal);
739                    if (aclChanged) {
740                        // If the Acl needed changing, change it now
741                        try {
742                            m_engine.getAclManager().setPermissions(page, page.getAcl());
743                        } catch (WikiSecurityException e) {
744                            log.error("Could not change page ACL for page " + page.getName() + ": " + e.getMessage(), e);
745                        }
746                        pagesChanged++;
747                    }
748                }
749                log.info("Profile name change for '" + newPrincipal.toString() +
750                        "' caused " + pagesChanged + " page ACLs to change also.");
751            } catch (ProviderException e) {
752                // Oooo! This is really bad...
753                log.error("Could not change user name in Page ACLs because of Provider error:" + e.getMessage(), e);
754            }
755        }
756    }
757
758    /**
759     * For a single wiki page, replaces all Acl entries matching a supplied array of Principals
760     * with a new Principal.
761     *
762     * @param page          the wiki page whose Acl is to be modified
763     * @param oldPrincipals an array of Principals to replace; all AclEntry objects whose
764     *                      {@link AclEntry#getPrincipal()} method returns one of these Principals will be replaced
765     * @param newPrincipal  the Principal that should receive the old Principals' permissions
766     * @return <code>true</code> if the Acl was actually changed; <code>false</code> otherwise
767     */
768    protected boolean changeAcl(WikiPage page, Principal[] oldPrincipals, Principal newPrincipal) {
769        Acl acl = page.getAcl();
770        boolean pageChanged = false;
771        if (acl != null) {
772            Enumeration<AclEntry> entries = acl.entries();
773            Collection<AclEntry> entriesToAdd = new ArrayList<AclEntry>();
774            Collection<AclEntry> entriesToRemove = new ArrayList<AclEntry>();
775            while (entries.hasMoreElements()) {
776                AclEntry entry = entries.nextElement();
777                if (ArrayUtils.contains(oldPrincipals, entry.getPrincipal())) {
778                    // Create new entry
779                    AclEntry newEntry = new AclEntryImpl();
780                    newEntry.setPrincipal(newPrincipal);
781                    Enumeration<Permission> permissions = entry.permissions();
782                    while (permissions.hasMoreElements()) {
783                        Permission permission = permissions.nextElement();
784                        newEntry.addPermission(permission);
785                    }
786                    pageChanged = true;
787                    entriesToRemove.add(entry);
788                    entriesToAdd.add(newEntry);
789                }
790            }
791            for (Iterator<AclEntry> ix = entriesToRemove.iterator(); ix.hasNext(); ) {
792                AclEntry entry = ix.next();
793                acl.removeEntry(entry);
794            }
795            for (Iterator<AclEntry> ix = entriesToAdd.iterator(); ix.hasNext(); ) {
796                AclEntry entry = ix.next();
797                acl.addEntry(entry);
798            }
799        }
800        return pageChanged;
801    }
802
803}