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