001: /*
002: JSPWiki - a JSP-based WikiWiki clone.
003:
004: Copyright (C) 2001-2006 Janne Jalkanen (Janne.Jalkanen@iki.fi)
005:
006: This program is free software; you can redistribute it and/or modify
007: it under the terms of the GNU Lesser General Public License as published by
008: the Free Software Foundation; either version 2.1 of the License, or
009: (at your option) any later version.
010:
011: This program is distributed in the hope that it will be useful,
012: but WITHOUT ANY WARRANTY; without even the implied warranty of
013: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014: GNU Lesser General Public License for more details.
015:
016: You should have received a copy of the GNU Lesser General Public License
017: along with this program; if not, write to the Free Software
018: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
019: */
020: package com.ecyrd.jspwiki;
021:
022: import java.util.*;
023:
024: import org.apache.log4j.Logger;
025: import org.apache.oro.text.regex.*;
026:
027: import com.ecyrd.jspwiki.attachment.Attachment;
028: import com.ecyrd.jspwiki.parser.JSPWikiMarkupParser;
029: import com.ecyrd.jspwiki.parser.MarkupParser;
030: import com.ecyrd.jspwiki.providers.ProviderException;
031: import com.ecyrd.jspwiki.providers.WikiAttachmentProvider;
032: import com.ecyrd.jspwiki.providers.WikiPageProvider;
033:
034: /**
035: * Do all the nitty-gritty work of renaming pages.
036: *
037: * @since 2.4
038: */
039: public class PageRenamer {
040: private static final Logger log = Logger
041: .getLogger(PageRenamer.class);
042:
043: private final WikiEngine m_wikiEngine;
044:
045: private static final PatternMatcher MATCHER = new Perl5Matcher();
046:
047: private boolean m_camelCaseLink;
048: private boolean m_matchEnglishPlurals;
049:
050: private static final String LONG_LINK_PATTERN = "\\[([\\w\\s]+\\|)?([\\w\\s\\+-/\\?&;@:=%\\#<>$\\.,\\(\\)'\\*]+)?\\]";
051: private static final String CAMELCASE_LINK_PATTERN = "([[:upper:]]+[[:lower:]]+[[:upper:]]+[[:alnum:]]*)";
052:
053: private Pattern m_longLinkPattern = null;
054: private Pattern m_camelCaseLinkPattern = null;
055:
056: /**
057: * Constructor, ties this renamer instance to a WikiEngine.
058: * @param engine the wiki engine
059: * @param props the properties used to initialize the wiki engine
060: */
061: public PageRenamer(WikiEngine engine, Properties props) {
062: m_wikiEngine = engine;
063:
064: // Retrieve relevant options
065: m_matchEnglishPlurals = TextUtil.getBooleanProperty(props,
066: WikiEngine.PROP_MATCHPLURALS, false);
067:
068: m_camelCaseLink = TextUtil.getBooleanProperty(props,
069: JSPWikiMarkupParser.PROP_CAMELCASELINKS, false);
070:
071: // Compile regular expression patterns
072: PatternCompiler compiler = new Perl5Compiler();
073:
074: try {
075: m_longLinkPattern = compiler.compile(LONG_LINK_PATTERN);
076: m_camelCaseLinkPattern = compiler
077: .compile(CAMELCASE_LINK_PATTERN);
078: } catch (MalformedPatternException mpe) {
079: log.error("Error compiling regexp patterns.", mpe);
080: }
081: }
082:
083: /**
084: * Renames, or moves, a wiki page. Can also alter referring wiki
085: * links to point to the renamed page.
086: * @param context TODO
087: * @param oldName Name of the source page.
088: * @param newName Name of the destination page.
089: * @param changeReferrers If true, then changes any referring links
090: * to point to the renamed page.
091: *
092: * @return The name of the page that the source was renamed to.
093: *
094: * @throws WikiException In the case of an error, such as the destination
095: * page already existing.
096: */
097: public String renamePage(WikiContext context, String oldName,
098: String newName, boolean changeReferrers)
099: throws WikiException {
100: // Work out the clean version of the new name of the page
101: newName = MarkupParser.cleanLink(newName.trim());
102:
103: // Get the collection of pages that the refered to the old name (the From name)...
104: Collection referrers = getReferrersCollection(oldName);
105:
106: log.debug("Rename request for page '" + oldName + "' to '"
107: + newName + "'");
108:
109: // Check if we're attempting to rename to a pagename that already exists
110: if (m_wikiEngine.pageExists(newName)) {
111: log.debug("Rename request failed because target page '"
112: + newName + "' exists");
113:
114: throw new WikiException("Page exists");
115: }
116:
117: // Tell the providers to actually move the data around...
118: movePageData(oldName, newName);
119: moveAttachmentData(oldName, newName);
120:
121: m_wikiEngine.getReferenceManager().clearPageEntries(oldName);
122:
123: // If there were pages refering to the old name, update them to point to the new name...
124: if (referrers != null) {
125: updateReferrersOnRename(context, oldName, newName,
126: changeReferrers, referrers);
127: } else {
128: // Now we need to go and update.
129: WikiPage p = m_wikiEngine.getPage(newName);
130: String pagedata = m_wikiEngine.getPureText(p);
131: Collection refs = m_wikiEngine.scanWikiLinks(p, pagedata);
132: m_wikiEngine.getReferenceManager().updateReferences(
133: newName, refs);
134: }
135:
136: return newName;
137: }
138:
139: // Go gather and return a collection of page names that refer to the old name...
140: private Collection getReferrersCollection(String oldName) {
141: TreeSet list = new TreeSet();
142:
143: WikiPage p = m_wikiEngine.getPage(oldName);
144:
145: if (p != null) {
146: Collection c = m_wikiEngine.getReferenceManager()
147: .findReferrers(oldName);
148:
149: if (c != null)
150: list.addAll(c);
151:
152: try {
153: Collection attachments = m_wikiEngine
154: .getAttachmentManager().listAttachments(p);
155:
156: for (Iterator i = attachments.iterator(); i.hasNext();) {
157: Attachment att = (Attachment) i.next();
158:
159: c = m_wikiEngine.getReferenceManager()
160: .findReferrers(att.getName());
161:
162: if (c != null)
163: list.addAll(c);
164: }
165: } catch (ProviderException e) {
166: log.error("Cannot list attachments", e);
167: }
168:
169: }
170:
171: return list;
172: }
173:
174: // Loop the collection, calling update for each, tickle the reference manager when done.
175: private void updateReferrersOnRename(WikiContext context,
176: String oldName, String newName, boolean changeReferrers,
177: Collection referrers) {
178: // Make a new list out of this, otherwise there is a ConcurrentModificationException
179: // when the referrer is modifed at the end of this loop when it no longer refers to
180: // the original page.
181: List referrersList = new ArrayList(referrers);
182: Iterator referrersIterator = referrersList.iterator();
183: while (referrersIterator.hasNext()) {
184: String referrerName = (String) referrersIterator.next();
185: updateReferrerOnRename(context, oldName, newName,
186: changeReferrers, referrerName);
187: }
188:
189: // Manage self-references, which the RefMgr is not managing for us
190:
191: updateReferrerOnRename(context, oldName, newName,
192: changeReferrers, newName);
193:
194: m_wikiEngine.getReferenceManager().clearPageEntries(oldName);
195:
196: String text = m_wikiEngine.getText(newName);
197:
198: Collection updatedReferrers = m_wikiEngine.scanWikiLinks(
199: m_wikiEngine.getPage(newName), text);
200: m_wikiEngine.getReferenceManager().updateReferences(newName,
201: updatedReferrers);
202: }
203:
204: // Update the referer, changing text if indicated.
205: private void updateReferrerOnRename(WikiContext context,
206: String oldName, String newName, boolean changeReferrer,
207: String referrerName) {
208: log.debug("oldName = " + oldName);
209: log.debug("newName = " + newName);
210: log.debug("referrerName = " + referrerName);
211:
212: String text = m_wikiEngine.getPureText(referrerName,
213: WikiProvider.LATEST_VERSION);
214:
215: if (changeReferrer) {
216: text = changeReferrerText(oldName, newName, referrerName,
217: text);
218: }
219:
220: try {
221:
222: WikiContext tempCtx = new WikiContext(m_wikiEngine,
223: m_wikiEngine.getPage(referrerName));
224:
225: if (context.getPage() != null) {
226: PageLock lock = m_wikiEngine.getPageManager()
227: .getCurrentLock(context.getPage());
228: m_wikiEngine.getPageManager().unlockPage(lock);
229:
230: tempCtx.getPage().setAuthor(
231: context.getCurrentUser().getName());
232: m_wikiEngine.saveText(tempCtx, text);
233:
234: Collection updatedReferrers = m_wikiEngine
235: .scanWikiLinks(m_wikiEngine
236: .getPage(referrerName), text);
237:
238: m_wikiEngine.getReferenceManager().updateReferences(
239: referrerName, updatedReferrers);
240: }
241: } catch (WikiException e) {
242: log.error("Unable to update referer on rename!", e);
243: }
244:
245: }
246:
247: /**
248: * Change the text of each referer to reflect the renamed page. There are seven starting cases
249: * and two differnting ending scenarios depending on if the new name is camel or long.
250: * <pre>
251: * "Start" "A" "B"
252: * 1) OldCleanLink --> NewCleanLink --> [New Long Link]
253: * 2) [OldCleanLink] --> [NewCleanLink] --> [New Long Link]
254: * 3) [old long text|OldCleanLink] --> [old long text|NewCleanLink] --> [old long text|New Long Link]
255: * 4) [Old Long Link] --> [NewCleanLink] --> [New Long Link]
256: * 5) [old long text|Old Long Link] --> [old long text|NewCleanLink] --> [old long text|New Long Link]
257: * 6) OldLongLink --> NewCleanLink --> NewLongLink
258: * 7) [OldLongLink] --> [NewCleanLink] --> [NewLongLink]
259: * </pre>
260: * It's important to note that case 6 and 7 can exist, but are not expected since they are
261: * counter intuitive.
262: * <br/>
263: * When doing any of the above renames these should not get touched...
264: * A) OtherOldCleanLink
265: * B) ~OldCleanLink <-Um maybe we _should_ rename this one?
266: * C) [[OldCleanLink] <-Maybe rename this too?
267: */
268: private String changeReferrerText(String oldName, String newName,
269: String referrerName, String referrerText) {
270: // The text we are replacing old links with
271: // String replacementLink = null;
272:
273: // Work out whether the new page name is CamelCase or not
274: // TODO: Check if the pattern can be replaced with the compiled version
275: /*
276: if( m_camelCaseLink == false || !m_perlUtil.match( "/" + m_camelCaseLinkPatternString + "/", newName ) )
277: {
278: replacementLink = "["+newName+"]";
279: }
280: else
281: {
282: replacementLink = newName;
283: }
284: */
285: // replacementLink = "["+newName+"]";
286: // Replace long format links
287: referrerText = replaceLongLinks(referrerText, oldName, newName);
288:
289: // Replace CamelCase links
290: if (m_camelCaseLink == true) {
291: referrerText = replaceCamelCaseLinks(referrerText, oldName,
292: newName);
293: }
294:
295: return referrerText;
296: }
297:
298: /**
299: * Replace long format links in a piece of text
300: */
301: private String replaceLongLinks(String text, String oldName,
302: String replacementLink) {
303: int lastMatchEnd = 0;
304:
305: PatternMatcherInput input = new PatternMatcherInput(text);
306:
307: StringBuffer ret = new StringBuffer();
308:
309: while (MATCHER.contains(input, m_longLinkPattern)) {
310: MatchResult matchResult = MATCHER.getMatch();
311:
312: ret.append(input.substring(lastMatchEnd, matchResult
313: .beginOffset(0)));
314:
315: String linkText = matchResult.group(1);
316: String link = matchResult.group(2);
317:
318: String anchor = "";
319: String subpage = "";
320:
321: if (link == null) {
322: throw new InternalWikiException(
323: "Null link while trying to rename! Culprit text is "
324: + text);
325: }
326:
327: int hash;
328: if ((hash = link.indexOf('#')) != -1) {
329: anchor = link.substring(hash);
330: link = link.substring(0, hash);
331: }
332:
333: int slash;
334: if ((slash = link.indexOf('/')) != -1) {
335: subpage = link.substring(slash);
336: link = link.substring(0, slash);
337: }
338:
339: String linkDestinationPage = checkPluralPageName(MarkupParser
340: .cleanLink(link));
341:
342: if (linkDestinationPage.equals(oldName)) {
343: String properReplacement;
344:
345: if (linkText != null) {
346: properReplacement = '[' + linkText
347: + replacementLink + subpage + anchor + ']';
348: } else {
349: properReplacement = '[' + replacementLink + subpage
350: + anchor + ']';
351: }
352:
353: ret.append(properReplacement);
354: } else {
355: ret.append(input.substring(matchResult.beginOffset(0),
356: matchResult.endOffset(0)));
357: }
358:
359: lastMatchEnd = matchResult.endOffset(0);
360: }
361:
362: ret.append(input.substring(lastMatchEnd));
363:
364: return ret.toString();
365: }
366:
367: // Replace CamelCase format links in a piece of text
368: private String replaceCamelCaseLinks(String text, String oldName,
369: String replacementLink) {
370: int lastMatchEnd = 0;
371:
372: PatternMatcherInput input = new PatternMatcherInput(text);
373:
374: StringBuffer ret = new StringBuffer();
375:
376: while (MATCHER.contains(input, m_camelCaseLinkPattern)) {
377: MatchResult matchResult = MATCHER.getMatch();
378:
379: ret.append(input.substring(lastMatchEnd, matchResult
380: .beginOffset(0)));
381:
382: // Check if there's the tilde to stop this being a camel case link
383: int matchOffset = matchResult.beginOffset(0);
384:
385: char charBefore = 0;
386:
387: if (matchOffset != 0) {
388: charBefore = input.charAt(matchOffset - 1);
389: }
390:
391: // Check if the CamelCase link has been escaped
392: if (charBefore != '~') {
393: // Check if this link maps to our page
394: String page = checkPluralPageName(matchResult.group(0));
395:
396: if (page.equals(oldName)) {
397: ret.append(replacementLink);
398: } else {
399: ret.append(input.substring(matchResult
400: .beginOffset(0), matchResult.endOffset(0)));
401: }
402: } else {
403: ret.append(input.substring(matchResult.beginOffset(0),
404: matchResult.endOffset(0)));
405: }
406:
407: lastMatchEnd = matchResult.endOffset(0);
408: }
409:
410: ret.append(input.substring(lastMatchEnd));
411:
412: return ret.toString();
413: }
414:
415: /**
416: * Checks if a name is plural, and if so, checks if the page with plural
417: * exists, otherwise it returns the singular version of the name.
418: * @param pageName the name of the page
419: * @return the corrected page name
420: */
421: public String checkPluralPageName(String pageName) {
422: if (pageName == null) {
423: return null;
424: }
425:
426: if (m_matchEnglishPlurals) {
427: try {
428: if (pageName.endsWith("s")
429: && !m_wikiEngine.getPageManager().pageExists(
430: pageName)) {
431: pageName = pageName.substring(0,
432: pageName.length() - 1);
433: }
434: } catch (ProviderException e) {
435: log.error("Unable to check Plural Pagename!", e);
436: }
437: }
438:
439: return pageName;
440: }
441:
442: //Move the page data from the old name to the new name.
443: private void movePageData(String oldName, String newName)
444: throws WikiException {
445: WikiPageProvider pageProvider = m_wikiEngine.getPageManager()
446: .getProvider();
447:
448: try {
449: pageProvider.movePage(oldName, newName);
450: } catch (ProviderException pe) {
451: log.debug("Failed in .movePageData()", pe);
452: throw new WikiException(pe.getMessage());
453: }
454: }
455:
456: //Move the attachment data from the old name to the new name.
457: private void moveAttachmentData(String oldName, String newName)
458: throws WikiException {
459: WikiAttachmentProvider attachmentProvider = m_wikiEngine
460: .getAttachmentManager().getCurrentProvider();
461:
462: log.debug("Trying to move all attachments from old page name "
463: + oldName + " to new page name " + newName);
464:
465: try {
466: attachmentProvider.moveAttachmentsForPage(oldName, newName);
467: //moveAttachmentsForPage(oldName, newName);
468: } catch (ProviderException pe) {
469: log.debug("Failed in .moveAttachmentData()", pe);
470: throw new WikiException(pe.getMessage());
471: }
472: }
473: }
|