/*
* WPCleaner: A tool to help on Wikipedia maintenance tasks.
* Copyright (C) 2013 Nicolas Vervelle
*
* See README.txt file for licensing information.
*/
package org.wikipediacleaner.api.data;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wikipediacleaner.utils.Configuration;
/**
* Suggestions for text replacements.
*/
public class Suggestion implements Comparable<Suggestion> {
private final static Log log = LogFactory.getLog(Suggestion.class);
private final static String TAG_NOWIKI_1 = "<nowiki>";
private final static String TAG_NOWIKI_2 = "</nowiki>";
/**
* Page and chapter in which the suggestion is defined.
*/
private final String chapter;
/**
* Regular expression pattern.
*/
private final Pattern pattern;
/**
* True if the pattern is not a native WPCleaner pattern (AWB, ...)
*/
private final boolean other;
/**
* List of possible replacements.
*/
private final List<ElementarySuggestion> suggestions;
/**
* Comment for the replacements.
*/
private String comment;
/**
* Clean a non WPCleaner pattern.
*
* @param patternText Original pattern.
* @return Cleaned up pattern.
*/
public static String cleanPattern(String patternText) {
if (patternText == null) {
return null;
}
// Check for {{ or }}
int lastIndex = 0;
int currentIndex = 0;
StringBuilder tmpPattern = new StringBuilder();
while (currentIndex < patternText.length()) {
if (patternText.startsWith("{{", currentIndex)) {
if (currentIndex > lastIndex) {
tmpPattern.append(patternText.substring(lastIndex, currentIndex));
}
tmpPattern.append("\\{\\{");
currentIndex += 2;
lastIndex = currentIndex;
} else if (patternText.startsWith("}}", currentIndex)) {
if (currentIndex > lastIndex) {
tmpPattern.append(patternText.substring(lastIndex, currentIndex));
}
tmpPattern.append("\\}\\}}");
currentIndex += 2;
lastIndex = currentIndex;
} else {
currentIndex++;
}
}
if (currentIndex > 0) {
if (lastIndex < patternText.length()) {
tmpPattern.append(patternText.substring(lastIndex));
}
patternText = tmpPattern.toString();
}
return patternText;
}
/**
* Create a Suggestion.
*
* @param patternText Search pattern.
* @param other True if the pattern is not a native WPCleaner pattern.
* @param chapter Page and chapter in which the suggestion is defined.
* @return Suggestion or null if there's a problem.
*/
public static Suggestion createSuggestion(
String patternText, boolean other,
String chapter) {
try {
if ((patternText.startsWith(TAG_NOWIKI_1)) &&
(patternText.endsWith(TAG_NOWIKI_2))) {
patternText = patternText.substring(
TAG_NOWIKI_1.length(),
patternText.length() - TAG_NOWIKI_2.length());
}
Pattern pattern = Pattern.compile(patternText);
return new Suggestion(pattern, other, chapter);
} catch (PatternSyntaxException e) {
log.warn("Incorrect pattern syntax for [" + patternText + "]: " + e.getMessage());
}
return null;
}
/**
* @param pattern Search pattern.
* @param other True if the pattern is not a native WPCleaner pattern.
* @param chapter Page and chapter in which the suggestion is defined.
*/
private Suggestion(
Pattern pattern, boolean other,
String chapter) {
this.chapter = chapter;
this.pattern = pattern;
this.other = other;
this.suggestions = new ArrayList<ElementarySuggestion>();
this.comment = null;
}
/**
* @return Page and chapter in which the suggestion is defined.
*/
public String getChapter() {
return chapter;
}
/**
* @return True if the pattern is not a native WPCleaner pattern.
*/
public boolean isOtherPattern() {
return other;
}
/**
* @return Search pattern.
*/
public String getPatternText() {
return pattern.pattern();
}
/**
* Add a possible replacement.
*
* @param replacement Replacement.
* @param automatic True if replacement can be done automatically.
*/
public void addReplacement(String replacement, boolean automatic) {
if (replacement != null) {
if ((replacement.startsWith(TAG_NOWIKI_1)) &&
(replacement.endsWith(TAG_NOWIKI_2))) {
replacement = replacement.substring(
TAG_NOWIKI_1.length(),
replacement.length() - TAG_NOWIKI_2.length());
}
boolean added = false;
for (ElementarySuggestion suggestion : suggestions) {
if (replacement.equals(suggestion.getReplacement())) {
added = true;
if (automatic) {
suggestion.setAutomatic();
}
}
}
if (!added) {
suggestions.add(new ElementarySuggestion(replacement, automatic));
}
}
}
/**
* @param comment Comment.
*/
public void setComment(String comment) {
this.comment = comment;
}
/**
* @return Comment.
*/
public String getComment() {
return comment;
}
/**
* @param text Text to look at.
* @return A matcher for the pattern
*/
public Matcher initMatcher(String text) {
Matcher matcher = pattern.matcher(text);
matcher.useAnchoringBounds(false);
matcher.useTransparentBounds(true);
return matcher;
}
/**
* @return True if at least some replacements are automatic.
*/
public boolean hasAutomaticReplacements() {
if (suggestions != null) {
for (ElementarySuggestion suggestion : suggestions) {
if (suggestion.isAutomatic()) {
return true;
}
}
}
return false;
}
/**
* @param initialText Initial text.
* @return Possible replacements.
*/
public List<ElementarySuggestion> getReplacements(
String initialText, int begin, int end) {
List<ElementarySuggestion> list = new ArrayList<ElementarySuggestion>();
for (ElementarySuggestion suggestion : suggestions) {
String replacement = suggestion.getReplacement();
try {
String newText = pattern.matcher(initialText).replaceFirst(replacement);
String initialPrefix = initialText.substring(0, Math.min(begin, initialText.length()));
String newPrefix = newText.substring(0, Math.min(begin, initialText.length()));
String initialSuffix = initialText.substring(end);
String newSuffix = newText.substring(Math.max(newText.length() - initialText.length() + end, 0));
if (initialPrefix.equals(newPrefix) && initialSuffix.equals(newSuffix)) {
newText = newText.substring(newPrefix.length(), newText.length() - newSuffix.length());
boolean added = false;
for (ElementarySuggestion element : list) {
if (newText.equals(element.getReplacement())) {
added = true;
}
}
if (!added) {
list.add(new ElementarySuggestion(newText, suggestion.isAutomatic()));
}
}
} catch (Exception e) {
log.error(
"Error when trying to get replacement\n " +
initialText.substring(begin, end) + " → " + replacement +
"\n " + e.getMessage());
}
}
return list;
}
// ==========================================================================
// Chapters management
// ==========================================================================
/**
* List of inactive chapters.
*/
private static List<String> inactiveChapters;
private final static Object lockClass = new Object();
/**
* Initialize list of inactive chapters.
*/
private static void initializeInactiveChapters() {
synchronized (lockClass) {
if (inactiveChapters == null) {
Configuration config = Configuration.getConfiguration();
inactiveChapters = config.getStringList(null, Configuration.ARRAY_SPELLING_INACTIVE);
}
}
}
/**
* @param chapter Chapter.
* @return True if the chapter is active.
*/
public static boolean isChapterActive(String chapter) {
initializeInactiveChapters();
return !inactiveChapters.contains(chapter);
}
/**
* @param page Page.
* @param title Title.
* @return True if the chapter is active.
*/
public static boolean isChapterActive(String page, String title) {
initializeInactiveChapters();
return !inactiveChapters.contains(page + "#" + title);
}
/**
* @return True if the suggestion is active.
*/
public boolean isActive() {
initializeInactiveChapters();
return isChapterActive(chapter);
}
/**
* Activate or deactivate a list of chapters.
*
* @param page Page.
* @param chapters Chapters.
* @param activate True to activate, false to deactivate.
*/
public static void activateChapters(String page, List<String> chapters, boolean activate) {
initializeInactiveChapters();
if ((chapters == null) || (chapters.isEmpty())) {
return;
}
for (String chapter : chapters) {
String chapterName = (page != null ? page + "#" : "") + chapter;
if (activate) {
inactiveChapters.remove(chapterName);
} else if (!inactiveChapters.contains(chapterName)) {
inactiveChapters.add(chapterName);
Collections.sort(inactiveChapters);
}
}
Configuration config = Configuration.getConfiguration();
config.setStringList(null, Configuration.ARRAY_SPELLING_INACTIVE, inactiveChapters);
}
/**
* Construct a list of chapters containing suggestions.
*
* @param suggestions List of suggestions.
* @return List of chapters containing suggestions (Page => Chapters).
*/
public static Map<String, List<String>> getChapters(Collection<Suggestion> suggestions) {
Map<String, List<String>> chapters = new HashMap<String, List<String>>();
for (Suggestion suggestion : suggestions) {
String chapter = suggestion.getChapter();
int sharpIndex = chapter.indexOf('#');
String page = (sharpIndex < 0) ? chapter : chapter.substring(0, sharpIndex);
String title = (sharpIndex < 0) ? "" : chapter.substring(sharpIndex + 1);
List<String> pageChapters = chapters.get(page);
if (pageChapters == null) {
pageChapters = new ArrayList<String>();
chapters.put(page, pageChapters);
}
if (!pageChapters.contains(title)) {
pageChapters.add(title);
Collections.sort(pageChapters);
}
}
return chapters;
}
// ==========================================================================
// Comparable interface
// ==========================================================================
/**
* @param o the object to be compared.
* @return a negative integer, zero, or a positive integer as this object
* is less than, equal to, or greater than the specified object.
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(Suggestion o) {
if (o == null) {
return -1;
}
if (this.other != o.other) {
return this.other ? 1 : -1;
}
if (this.comment != null) {
if (o.comment == null) {
return -1;
}
return this.comment.compareTo(o.comment);
}
if (o.comment != null) {
return 1;
}
return 0;
}
/**
* Bean for holding an elementary suggestion.
*/
public static class ElementarySuggestion {
/**
* Replacement string.
*/
private final String replacement;
/**
* True if the replacement can be done automatically.
*/
private boolean automatic;
/**
* @param replacement Replacement string.
* @param automatic True if the replacement can be done automatically.
*/
public ElementarySuggestion(String replacement, boolean automatic) {
this.replacement = replacement;
this.automatic = automatic;
}
/**
* @return Replacement string.
*/
public String getReplacement() {
return replacement;
}
/**
* @return True if the replacement can be done automatically.
*/
public boolean isAutomatic() {
return automatic;
}
/**
* Call to mark the replacement to be done automatically.
*/
public void setAutomatic() {
this.automatic = true;
}
}
}