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.content;
020
021import java.util.Collection;
022import java.util.Set;
023import java.util.TreeSet;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.apache.log4j.Logger;
028import org.apache.wiki.InternalWikiException;
029import org.apache.wiki.WikiContext;
030import org.apache.wiki.WikiEngine;
031import org.apache.wiki.WikiPage;
032import org.apache.wiki.api.exceptions.ProviderException;
033import org.apache.wiki.api.exceptions.WikiException;
034import org.apache.wiki.attachment.Attachment;
035import org.apache.wiki.event.WikiEventManager;
036import org.apache.wiki.event.WikiPageRenameEvent;
037import org.apache.wiki.parser.JSPWikiMarkupParser;
038import org.apache.wiki.parser.MarkupParser;
039import org.apache.wiki.util.TextUtil;
040
041/**
042 *  Provides page renaming functionality.  Note that there used to be
043 *  a similarly named class in 2.6, but due to unclear copyright, the
044 *  class was completely rewritten from scratch for 2.8.
045 *
046 *  @since 2.8
047 */
048public class PageRenamer
049{
050
051    private static final Logger log = Logger.getLogger( PageRenamer.class );
052    
053    private boolean m_camelCase = false;
054    
055    /**
056     *  Renames a page.
057     *  
058     *  @param context The current context.
059     *  @param renameFrom The name from which to rename.
060     *  @param renameTo The new name.
061     *  @param changeReferrers If true, also changes all the referrers.
062     *  @return The final new name (in case it had to be modified)
063     *  @throws WikiException If the page cannot be renamed.
064     */
065    public String renamePage(final WikiContext context, 
066                              final String renameFrom, 
067                              final String renameTo, 
068                              final boolean changeReferrers )
069        throws WikiException
070    {
071        //
072        //  Sanity checks first
073        //
074        if( renameFrom == null || renameFrom.length() == 0 )
075        {
076            throw new WikiException( "From name may not be null or empty" );
077        }
078        if( renameTo == null || renameTo.length() == 0 )
079        {
080            throw new WikiException( "To name may not be null or empty" );
081        }
082       
083        //
084        //  Clean up the "to" -name so that it does not contain anything illegal
085        //
086        
087        String renameToClean = MarkupParser.cleanLink( renameTo.trim() );
088        
089        if( renameToClean.equals(renameFrom) )
090        {
091            throw new WikiException( "You cannot rename the page to itself" );
092        }
093        
094        //
095        //  Preconditions: "from" page must exist, and "to" page must not yet exist.
096        //
097        WikiEngine engine = context.getEngine();
098        WikiPage fromPage = engine.getPage( renameFrom );
099        
100        if( fromPage == null )
101        {
102            throw new WikiException("No such page "+renameFrom);
103        }
104        
105        WikiPage toPage = engine.getPage( renameToClean );
106        
107        if( toPage != null )
108        {
109            throw new WikiException( "Page already exists " + renameToClean );
110        }
111        
112        //
113        //  Options
114        //
115        
116        m_camelCase = TextUtil.getBooleanProperty( engine.getWikiProperties(), 
117                                                   JSPWikiMarkupParser.PROP_CAMELCASELINKS, 
118                                                   m_camelCase );
119
120        Set<String> referrers = getReferencesToChange( fromPage, engine );
121
122        //
123        //  Do the actual rename by changing from the frompage to the topage, including
124        //  all of the attachments
125        //
126        
127        //  Remove references to attachments under old name
128        @SuppressWarnings( "unchecked" )
129        Collection<Attachment> attachmentsOldName = engine.getAttachmentManager().listAttachments( fromPage );
130        for (Attachment att:attachmentsOldName)
131        {
132            WikiPage fromAttPage = engine.getPage( att.getName() );
133            engine.getReferenceManager().pageRemoved( fromAttPage );
134        }
135
136        engine.getPageManager().getProvider().movePage( renameFrom, renameToClean );
137
138        if( engine.getAttachmentManager().attachmentsEnabled() )
139        {
140            engine.getAttachmentManager().getCurrentProvider().moveAttachmentsForPage( renameFrom, renameToClean );
141        }
142        
143        //
144        //  Add a comment to the page notifying what changed.  This adds a new revision
145        //  to the repo with no actual change.
146        //
147        
148        toPage = engine.getPage( renameToClean );
149        
150        if( toPage == null ) throw new InternalWikiException("Rename seems to have failed for some strange reason - please check logs!");
151
152        toPage.setAttribute( WikiPage.CHANGENOTE, fromPage.getName() + " ==> " + toPage.getName() );
153        toPage.setAuthor( context.getCurrentUser().getName() );
154        
155        engine.getPageManager().putPageText( toPage, engine.getPureText( toPage ) );
156
157        //
158        //  Update the references
159        //
160        
161        engine.getReferenceManager().pageRemoved( fromPage );
162        engine.updateReferences( toPage );
163
164        //
165        //  Update referrers
166        //
167        if( changeReferrers )
168        {
169            updateReferrers( context, fromPage, toPage, referrers );
170        }
171
172        //
173        //  re-index the page including its attachments
174        //
175        engine.getSearchManager().reindexPage(toPage);
176        
177        @SuppressWarnings( "unchecked" )
178        Collection<Attachment> attachmentsNewName = engine.getAttachmentManager().listAttachments( toPage );
179        for (Attachment att:attachmentsNewName)
180        {
181            WikiPage toAttPage = engine.getPage( att.getName() );
182            // add reference to attachment under new page name
183            engine.updateReferences( toAttPage );
184            engine.getSearchManager().reindexPage(att);
185        }
186
187        // Currently not used internally by JSPWiki itself, but you can use it for something else.
188        WikiEventManager.fireEvent( this, new WikiPageRenameEvent( this, renameFrom, renameToClean ) );
189
190        //
191        //  Done, return the new name.
192        //
193        return renameToClean;
194    }
195
196    /**
197     *  This method finds all the pages which have anything to do with the fromPage and
198     *  change any referrers it can figure out in that page.
199     *  
200     *  @param context WikiContext in which we operate
201     *  @param fromPage The old page
202     *  @param toPage The new page
203     */
204    private void updateReferrers( WikiContext context, WikiPage fromPage, WikiPage toPage, Set<String>referrers )
205    {
206        WikiEngine engine = context.getEngine();
207        
208        if( referrers.isEmpty() ) return; // No referrers
209        
210        for( String pageName : referrers )
211        {
212            //  In case the page was just changed from under us, let's do this
213            //  small kludge.
214            if( pageName.equals( fromPage.getName() ) )
215            {
216                pageName = toPage.getName();
217            }
218            
219            WikiPage p = engine.getPage( pageName );
220            
221            String sourceText = engine.getPureText( p );
222            
223            String newText = replaceReferrerString( context, sourceText, fromPage.getName(), toPage.getName() );
224            
225            if( m_camelCase )
226                newText = replaceCCReferrerString( context, newText, fromPage.getName(), toPage.getName() );
227            
228            if( !sourceText.equals( newText ) )
229            {
230                p.setAttribute( WikiPage.CHANGENOTE, fromPage.getName()+" ==> "+toPage.getName() );
231                p.setAuthor( context.getCurrentUser().getName() );
232         
233                try
234                {
235                    engine.getPageManager().putPageText( p, newText );
236                    engine.updateReferences( p );
237                }
238                catch( ProviderException e )
239                {
240                    //
241                    //  We fail with an error, but we will try to continue to rename
242                    //  other referrers as well.
243                    //
244                    log.error("Unable to perform rename.",e);
245                }
246            }
247        }
248    }
249
250    private Set<String> getReferencesToChange( WikiPage fromPage, WikiEngine engine )
251    {
252        Set<String> referrers = new TreeSet<String>();
253        
254        @SuppressWarnings( "unchecked" )
255        Collection<String> r = engine.getReferenceManager().findReferrers( fromPage.getName() );
256        if( r != null ) referrers.addAll( r );
257        
258        try
259        {
260            @SuppressWarnings( "unchecked" )
261            Collection<Attachment> attachments = engine.getAttachmentManager().listAttachments( fromPage );
262
263            for( Attachment att : attachments  )
264            {
265                @SuppressWarnings( "unchecked" )
266                Collection<String> c = engine.getReferenceManager().findReferrers(att.getName());
267
268                if( c != null ) referrers.addAll(c);
269            }
270        }
271        catch( ProviderException e )
272        {
273            // We will continue despite this error
274            log.error( "Provider error while fetching attachments for rename", e );
275        }
276        return referrers;
277    }
278
279    /**
280     *  Replaces camelcase links.
281     */
282    private String replaceCCReferrerString( WikiContext context, String sourceText, String from, String to )
283    {
284        StringBuilder sb = new StringBuilder( sourceText.length()+32 );
285        
286        Pattern linkPattern = Pattern.compile( "\\p{Lu}+\\p{Ll}+\\p{Lu}+[\\p{L}\\p{Digit}]*" );
287        
288        Matcher matcher = linkPattern.matcher( sourceText );
289        
290        int start = 0;
291        
292        while( matcher.find(start) )
293        {
294            String match = matcher.group();
295
296            sb.append( sourceText.substring( start, matcher.start() ) );
297
298            int lastOpenBrace = sourceText.lastIndexOf( '[', matcher.start() );
299            int lastCloseBrace = sourceText.lastIndexOf( ']', matcher.start() );
300            
301            if( match.equals( from ) && lastCloseBrace >= lastOpenBrace )
302            {
303                sb.append( to );
304            }
305            else
306            {
307                sb.append( match );
308            }
309            
310            start = matcher.end();
311        }
312        
313        sb.append( sourceText.substring( start ) );
314        
315        return sb.toString();
316    }
317
318    private String replaceReferrerString( WikiContext context, String sourceText, String from, String to )
319    {
320        StringBuilder sb = new StringBuilder( sourceText.length()+32 );
321        
322        //
323        //  This monstrosity just looks for a JSPWiki link pattern.  But it is pretty
324        //  cool for a regexp, isn't it?  If you can understand this in a single reading,
325        //  you have way too much time in your hands.
326        //
327        Pattern linkPattern = Pattern.compile( "([\\[\\~]?)\\[([^\\|\\]]*)(\\|)?([^\\|\\]]*)(\\|)?([^\\|\\]]*)\\]" );
328        
329        Matcher matcher = linkPattern.matcher( sourceText );
330        
331        int start = 0;
332        
333        // System.out.println("====");
334        // System.out.println("SRC="+sourceText.trim());
335        while( matcher.find(start) )
336        {
337            char charBefore = (char)-1;
338            
339            if( matcher.start() > 0 ) 
340                charBefore = sourceText.charAt( matcher.start()-1 );
341            
342            if( matcher.group(1).length() > 0 || charBefore == '~' || charBefore == '[' ) 
343            {
344                //
345                //  Found an escape character, so I am escaping.
346                //
347                sb.append( sourceText.substring( start, matcher.end() ) );
348                start = matcher.end();
349                continue;
350            }
351
352            String text = matcher.group(2);
353            String link = matcher.group(4);
354            String attr = matcher.group(6);
355             
356            /*
357            System.out.println("MATCH="+matcher.group(0));
358            System.out.println("   text="+text);
359            System.out.println("   link="+link);
360            System.out.println("   attr="+attr);
361            */
362            if( link.length() == 0 )
363            {
364                text = replaceSingleLink( context, text, from, to );
365            }
366            else
367            {
368                link = replaceSingleLink( context, link, from, to );
369                
370                //
371                //  A very simple substitution, but should work for quite a few cases.
372                //
373                text = TextUtil.replaceString( text, from, to );
374            }
375        
376            //
377            //  Construct the new string
378            //
379            sb.append( sourceText.substring( start, matcher.start() ) );
380            sb.append( "["+text );
381            if( link.length() > 0 ) sb.append( "|" + link );
382            if( attr.length() > 0 ) sb.append( "|" + attr );
383            sb.append( "]" );
384            
385            start = matcher.end();
386        }
387        
388        sb.append( sourceText.substring( start ) );
389        
390        return sb.toString();
391    }
392
393    /**
394     *  This method does a correct replacement of a single link, taking into
395     *  account anchors and attachments.
396     */
397    private String replaceSingleLink( WikiContext context, String original, String from, String newlink )
398    {
399        int hash = original.indexOf( '#' );
400        int slash = original.indexOf( '/' );
401        String reallink = original;
402        String oldStyleRealLink;
403        
404        if( hash != -1 ) reallink = original.substring( 0, hash );
405        if( slash != -1 ) reallink = original.substring( 0,slash );
406        
407        reallink = MarkupParser.cleanLink( reallink );
408        oldStyleRealLink = MarkupParser.wikifyLink( reallink );
409        
410        //WikiPage realPage  = context.getEngine().getPage( reallink );
411        // WikiPage p2 = context.getEngine().getPage( from );
412        
413        // System.out.println("   "+reallink+" :: "+ from);
414        // System.out.println("   "+p+" :: "+p2);
415        
416        //
417        //  Yes, these point to the same page.
418        //
419        if( reallink.equals(from) || original.equals(from) || oldStyleRealLink.equals(from) )
420        {
421            //
422            //  if the original contains blanks, then we should introduce a link, for example:  [My Page]  =>  [My Page|My Renamed Page]
423            int blank = reallink.indexOf( " ");
424            
425            if( blank != -1 )
426            {
427                return original + "|" + newlink;
428            }
429            
430            return newlink + ((hash > 0) ? original.substring( hash ) : "") + ((slash > 0) ? original.substring( slash ) : "") ;
431        }
432        
433        return original;
434    }
435}