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( context, 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( context, 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 Context context, 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 Context context, 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( context, text, from, to ); 284 } else { 285 link = replaceSingleLink( context, 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 Context context, 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}