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