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.List; 023import java.util.Set; 024import java.util.TreeSet; 025import java.util.regex.Matcher; 026import java.util.regex.Pattern; 027 028import org.apache.log4j.Logger; 029import org.apache.wiki.InternalWikiException; 030import org.apache.wiki.WikiContext; 031import org.apache.wiki.WikiEngine; 032import org.apache.wiki.WikiPage; 033import org.apache.wiki.api.exceptions.ProviderException; 034import org.apache.wiki.api.exceptions.WikiException; 035import org.apache.wiki.attachment.Attachment; 036import org.apache.wiki.event.WikiEventManager; 037import org.apache.wiki.event.WikiPageRenameEvent; 038import org.apache.wiki.parser.JSPWikiMarkupParser; 039import org.apache.wiki.parser.MarkupParser; 040import org.apache.wiki.util.TextUtil; 041 042/** 043 * Provides page renaming functionality. Note that there used to be 044 * a similarly named class in 2.6, but due to unclear copyright, the 045 * class was completely rewritten from scratch for 2.8. 046 * 047 * @since 2.8 048 */ 049public class PageRenamer 050{ 051 052 private static final Logger log = Logger.getLogger( PageRenamer.class ); 053 054 private boolean m_camelCase = false; 055 056 /** 057 * Renames a page. 058 * 059 * @param context The current context. 060 * @param renameFrom The name from which to rename. 061 * @param renameTo The new name. 062 * @param changeReferrers If true, also changes all the referrers. 063 * @return The final new name (in case it had to be modified) 064 * @throws WikiException If the page cannot be renamed. 065 */ 066 public String renamePage(final WikiContext context, 067 final String renameFrom, 068 final String renameTo, 069 final boolean changeReferrers ) 070 throws WikiException 071 { 072 // 073 // Sanity checks first 074 // 075 if( renameFrom == null || renameFrom.length() == 0 ) 076 { 077 throw new WikiException( "From name may not be null or empty" ); 078 } 079 if( renameTo == null || renameTo.length() == 0 ) 080 { 081 throw new WikiException( "To name may not be null or empty" ); 082 } 083 084 // 085 // Clean up the "to" -name so that it does not contain anything illegal 086 // 087 088 String renameToClean = MarkupParser.cleanLink( renameTo.trim() ); 089 090 if( renameToClean.equals(renameFrom) ) 091 { 092 throw new WikiException( "You cannot rename the page to itself" ); 093 } 094 095 // 096 // Preconditions: "from" page must exist, and "to" page must not yet exist. 097 // 098 WikiEngine engine = context.getEngine(); 099 WikiPage fromPage = engine.getPage( renameFrom ); 100 101 if( fromPage == null ) 102 { 103 throw new WikiException("No such page "+renameFrom); 104 } 105 106 WikiPage toPage = engine.getPage( renameToClean ); 107 108 if( toPage != null ) 109 { 110 throw new WikiException( "Page already exists " + renameToClean ); 111 } 112 113 // 114 // Options 115 // 116 117 m_camelCase = TextUtil.getBooleanProperty( engine.getWikiProperties(), 118 JSPWikiMarkupParser.PROP_CAMELCASELINKS, 119 m_camelCase ); 120 121 Set<String> referrers = getReferencesToChange( fromPage, engine ); 122 123 // 124 // Do the actual rename by changing from the frompage to the topage, including 125 // all of the attachments 126 // 127 128 // Remove references to attachments under old name 129 List<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 Collection<Attachment> attachmentsNewName = engine.getAttachmentManager().listAttachments( toPage ); 178 for (Attachment att:attachmentsNewName) 179 { 180 WikiPage toAttPage = engine.getPage( att.getName() ); 181 // add reference to attachment under new page name 182 engine.updateReferences( toAttPage ); 183 engine.getSearchManager().reindexPage(att); 184 } 185 186 // Currently not used internally by JSPWiki itself, but you can use it for something else. 187 WikiEventManager.fireEvent( this, new WikiPageRenameEvent( this, renameFrom, renameToClean ) ); 188 189 // 190 // Done, return the new name. 191 // 192 return renameToClean; 193 } 194 195 /** 196 * This method finds all the pages which have anything to do with the fromPage and 197 * change any referrers it can figure out in that page. 198 * 199 * @param context WikiContext in which we operate 200 * @param fromPage The old page 201 * @param toPage The new page 202 */ 203 private void updateReferrers( WikiContext context, WikiPage fromPage, WikiPage toPage, Set<String>referrers ) 204 { 205 WikiEngine engine = context.getEngine(); 206 207 if( referrers.isEmpty() ) return; // No referrers 208 209 for( String pageName : referrers ) 210 { 211 // In case the page was just changed from under us, let's do this 212 // small kludge. 213 if( pageName.equals( fromPage.getName() ) ) 214 { 215 pageName = toPage.getName(); 216 } 217 218 WikiPage p = engine.getPage( pageName ); 219 220 String sourceText = engine.getPureText( p ); 221 222 String newText = replaceReferrerString( context, sourceText, fromPage.getName(), toPage.getName() ); 223 224 if( m_camelCase ) 225 newText = replaceCCReferrerString( context, newText, fromPage.getName(), toPage.getName() ); 226 227 if( !sourceText.equals( newText ) ) 228 { 229 p.setAttribute( WikiPage.CHANGENOTE, fromPage.getName()+" ==> "+toPage.getName() ); 230 p.setAuthor( context.getCurrentUser().getName() ); 231 232 try 233 { 234 engine.getPageManager().putPageText( p, newText ); 235 engine.updateReferences( p ); 236 } 237 catch( ProviderException e ) 238 { 239 // 240 // We fail with an error, but we will try to continue to rename 241 // other referrers as well. 242 // 243 log.error("Unable to perform rename.",e); 244 } 245 } 246 } 247 } 248 249 private Set<String> getReferencesToChange( WikiPage fromPage, WikiEngine engine ) 250 { 251 Set<String> referrers = new TreeSet<String>(); 252 253 Collection<String> r = engine.getReferenceManager().findReferrers( fromPage.getName() ); 254 if( r != null ) referrers.addAll( r ); 255 256 try 257 { 258 List<Attachment> attachments = engine.getAttachmentManager().listAttachments( fromPage ); 259 260 for( Attachment att : attachments ) 261 { 262 Collection<String> c = engine.getReferenceManager().findReferrers(att.getName()); 263 264 if( c != null ) referrers.addAll(c); 265 } 266 } 267 catch( ProviderException e ) 268 { 269 // We will continue despite this error 270 log.error( "Provider error while fetching attachments for rename", e ); 271 } 272 return referrers; 273 } 274 275 /** 276 * Replaces camelcase links. 277 */ 278 private String replaceCCReferrerString( WikiContext context, String sourceText, String from, String to ) 279 { 280 StringBuilder sb = new StringBuilder( sourceText.length()+32 ); 281 282 Pattern linkPattern = Pattern.compile( "\\p{Lu}+\\p{Ll}+\\p{Lu}+[\\p{L}\\p{Digit}]*" ); 283 284 Matcher matcher = linkPattern.matcher( sourceText ); 285 286 int start = 0; 287 288 while( matcher.find(start) ) 289 { 290 String match = matcher.group(); 291 292 sb.append( sourceText.substring( start, matcher.start() ) ); 293 294 int lastOpenBrace = sourceText.lastIndexOf( '[', matcher.start() ); 295 int lastCloseBrace = sourceText.lastIndexOf( ']', matcher.start() ); 296 297 if( match.equals( from ) && lastCloseBrace >= lastOpenBrace ) 298 { 299 sb.append( to ); 300 } 301 else 302 { 303 sb.append( match ); 304 } 305 306 start = matcher.end(); 307 } 308 309 sb.append( sourceText.substring( start ) ); 310 311 return sb.toString(); 312 } 313 314 private String replaceReferrerString( WikiContext context, String sourceText, String from, String to ) 315 { 316 StringBuilder sb = new StringBuilder( sourceText.length()+32 ); 317 318 // 319 // This monstrosity just looks for a JSPWiki link pattern. But it is pretty 320 // cool for a regexp, isn't it? If you can understand this in a single reading, 321 // you have way too much time in your hands. 322 // 323 Pattern linkPattern = Pattern.compile( "([\\[\\~]?)\\[([^\\|\\]]*)(\\|)?([^\\|\\]]*)(\\|)?([^\\|\\]]*)\\]" ); 324 325 Matcher matcher = linkPattern.matcher( sourceText ); 326 327 int start = 0; 328 329 // System.out.println("===="); 330 // System.out.println("SRC="+sourceText.trim()); 331 while( matcher.find(start) ) 332 { 333 char charBefore = (char)-1; 334 335 if( matcher.start() > 0 ) 336 charBefore = sourceText.charAt( matcher.start()-1 ); 337 338 if( matcher.group(1).length() > 0 || charBefore == '~' || charBefore == '[' ) 339 { 340 // 341 // Found an escape character, so I am escaping. 342 // 343 sb.append( sourceText.substring( start, matcher.end() ) ); 344 start = matcher.end(); 345 continue; 346 } 347 348 String text = matcher.group(2); 349 String link = matcher.group(4); 350 String attr = matcher.group(6); 351 352 /* 353 System.out.println("MATCH="+matcher.group(0)); 354 System.out.println(" text="+text); 355 System.out.println(" link="+link); 356 System.out.println(" attr="+attr); 357 */ 358 if( link.length() == 0 ) 359 { 360 text = replaceSingleLink( context, text, from, to ); 361 } 362 else 363 { 364 link = replaceSingleLink( context, link, from, to ); 365 366 // 367 // A very simple substitution, but should work for quite a few cases. 368 // 369 text = TextUtil.replaceString( text, from, to ); 370 } 371 372 // 373 // Construct the new string 374 // 375 sb.append( sourceText.substring( start, matcher.start() ) ); 376 sb.append( "["+text ); 377 if( link.length() > 0 ) sb.append( "|" + link ); 378 if( attr.length() > 0 ) sb.append( "|" + attr ); 379 sb.append( "]" ); 380 381 start = matcher.end(); 382 } 383 384 sb.append( sourceText.substring( start ) ); 385 386 return sb.toString(); 387 } 388 389 /** 390 * This method does a correct replacement of a single link, taking into 391 * account anchors and attachments. 392 */ 393 private String replaceSingleLink( WikiContext context, String original, String from, String newlink ) 394 { 395 int hash = original.indexOf( '#' ); 396 int slash = original.indexOf( '/' ); 397 String reallink = original; 398 String oldStyleRealLink; 399 400 if( hash != -1 ) reallink = original.substring( 0, hash ); 401 if( slash != -1 ) reallink = original.substring( 0,slash ); 402 403 reallink = MarkupParser.cleanLink( reallink ); 404 oldStyleRealLink = MarkupParser.wikifyLink( reallink ); 405 406 //WikiPage realPage = context.getEngine().getPage( reallink ); 407 // WikiPage p2 = context.getEngine().getPage( from ); 408 409 // System.out.println(" "+reallink+" :: "+ from); 410 // System.out.println(" "+p+" :: "+p2); 411 412 // 413 // Yes, these point to the same page. 414 // 415 if( reallink.equals(from) || original.equals(from) || oldStyleRealLink.equals(from) ) 416 { 417 // 418 // if the original contains blanks, then we should introduce a link, for example: [My Page] => [My Page|My Renamed Page] 419 int blank = reallink.indexOf( " "); 420 421 if( blank != -1 ) 422 { 423 return original + "|" + newlink; 424 } 425 426 return newlink + ((hash > 0) ? original.substring( hash ) : "") + ((slash > 0) ? original.substring( slash ) : "") ; 427 } 428 429 return original; 430 } 431}