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}