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