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     */
019    package org.apache.wiki.content;
020    
021    import java.util.Collection;
022    import java.util.Set;
023    import java.util.TreeSet;
024    import java.util.regex.Matcher;
025    import java.util.regex.Pattern;
026    
027    import org.apache.log4j.Logger;
028    import org.apache.wiki.InternalWikiException;
029    import org.apache.wiki.WikiContext;
030    import org.apache.wiki.WikiEngine;
031    import org.apache.wiki.WikiPage;
032    import org.apache.wiki.api.exceptions.ProviderException;
033    import org.apache.wiki.api.exceptions.WikiException;
034    import org.apache.wiki.attachment.Attachment;
035    import org.apache.wiki.event.WikiEventManager;
036    import org.apache.wiki.event.WikiPageRenameEvent;
037    import org.apache.wiki.parser.JSPWikiMarkupParser;
038    import org.apache.wiki.parser.MarkupParser;
039    import 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     */
048    public 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    }