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