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