/*
* JETERS – Java Extensible Text Replacement System
* Copyright (C) 2006–2008 Tobias Knerr
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty
* of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
*/
package net.sf.jeters.components;
import static net.sf.jeters.componentInterface.dataStructs.UIRequest_String.LayoutFlag.lineWrap;
import static net.sf.jeters.componentInterface.dataStructs.UIRequest_String.LayoutFlag.multipleLines;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.apache.log4j.PropertyConfigurator;
import net.sf.jeters.componentInterface.InputComponent_Series;
import net.sf.jeters.componentInterface.OutputComponent;
import net.sf.jeters.componentInterface.UIComponent;
import net.sf.jeters.componentInterface.dataStructs.NamedDataNotAvailableException;
import net.sf.jeters.componentInterface.dataStructs.NamedDataSet;
import net.sf.jeters.componentInterface.dataStructs.UIRequest;
import net.sf.jeters.componentInterface.dataStructs.UIRequest_Boolean;
import net.sf.jeters.componentInterface.dataStructs.UIRequest_Output;
import net.sf.jeters.componentInterface.dataStructs.UIRequest_Selection;
import net.sf.jeters.componentInterface.dataStructs.UIRequest_String;
import net.sf.jeters.componentInterface.editables.MediaWikiText;
import net.sf.jeters.configuration.Conf;
import net.sf.jeters.configuration.NotifiedConfigurable;
import net.sf.jeters.util.AssistedTranslatable;
import net.sourceforge.jwbf.core.actions.util.ActionException;
import net.sourceforge.jwbf.core.actions.util.CookieException;
import net.sourceforge.jwbf.core.actions.util.ProcessException;
import net.sourceforge.jwbf.core.bots.util.JwbfException;
import net.sourceforge.jwbf.core.contentRep.ContentAccessable;
import net.sourceforge.jwbf.core.contentRep.SimpleArticle;
import net.sourceforge.jwbf.mediawiki.actions.queries.AllPageTitles;
import net.sourceforge.jwbf.mediawiki.actions.queries.BacklinkTitles;
import net.sourceforge.jwbf.mediawiki.actions.queries.CategoryMembersSimple;
import net.sourceforge.jwbf.mediawiki.actions.queries.ImageUsageTitles;
import net.sourceforge.jwbf.mediawiki.actions.queries.TemplateUserTitles;
import static net.sourceforge.jwbf.mediawiki.actions.util.RedirectFilter.*;
import net.sourceforge.jwbf.mediawiki.actions.util.VersionException;
import net.sourceforge.jwbf.mediawiki.bots.MediaWikiBot;
import edu.stanford.ejalbert.BrowserLauncher;
import edu.stanford.ejalbert.exception.BrowserLaunchingInitializingException;
import edu.stanford.ejalbert.exception.UnsupportedOperatingSystemException;
/**
* default implementation of a combined input and output component
* reading from and writing to any MediaWiki system.<br/>
*
* For output, an user account on the MediaWiki is necessary.
*
* @author Tobias Knerr
*/
public class MediaWikiIO extends AssistedTranslatable
implements InputComponent_Series<MediaWikiText>,
OutputComponent <MediaWikiText>,
NotifiedConfigurable {
/* settings */
@Conf private String[] conf_wikiURLs = new String[] {
"http://de.wikipedia.org/w/index.php",
"http://en.wikipedia.org/w/index.php",
"http://fr.wikipedia.org/w/index.php"};
@Conf private String username = "";
@Conf private String password = "";
@Conf private int restrictionsInArticleSelection = 1;
@Conf private boolean alwaysAskForOutputWiki = true;
@Conf private boolean showOverviewBeforeWriting = true;
@Conf private boolean showArticleInBrowser = false;
@Conf private boolean showPreviewInBrowser = false;
@Conf private String templatePrefix = "";
@Conf private String imagePrefix = "";
@Conf private String log4jConfigurationPath = "log4j.properties";
/** BrowserLauncher used to open pages and previews. Created when necessary. */
private BrowserLauncher browserLauncher = null;
/**
* an iterator providing a series of titles for articles that will be retured by input().
* may also be null, e.g. if only a single article was accessed.
*/
private Iterator<String> titleIterator = null;
/** Bot object */
private MediaWikiBot bot = null;
/** url currently used by bot */
private URL currentURL = null;
/* classes used to more elegantly apply restrictions to an title iterator */
/**
* abstract superclass for all restricted iterators,
* does not support remove method
*/
private static abstract class RestrictedIterator
implements Iterator<String> {
Iterator<String> iteratorToRestrict;
String next;
/**
* constructor
* @param iteratorToRestrict source of the Strings provided by this
* iterator; all iteratorToRestrict.next()
* results matching the restriction's
* criteria will be returned
* by this iterator's next()
*/
public RestrictedIterator(Iterator<String> iteratorToRestrict) {
this.iteratorToRestrict = iteratorToRestrict;
}
public boolean hasNext() {
if (next == null) {updateNext();}
return next != null;
}
public String next() {
if (next == null) {updateNext();}
String result = next;
updateNext();
return result;
}
public void remove() {
throw new UnsupportedOperationException();
}
/**
* finds the next acceptable title and sets this.next accordingly;
* if no title is left, next is set to null.
*/
private void updateNext() {
next = null;
while (next == null && iteratorToRestrict.hasNext()) {
String iteratorsNext = iteratorToRestrict.next();
if (isOk(iteratorsNext)) {
next = iteratorsNext;
}
}
}
/** decides whether the title matches the restriction's criteria */
protected abstract boolean isOk(String title);
}
/** requires title to be alphabetically >= given String */
private static class RestrictedIterator_StartFrom
extends RestrictedIterator {
private String startFrom;
/**
* constructor
* @param startFrom String all titles must be larger than
*/
public RestrictedIterator_StartFrom(Iterator<String> iteratorToRestrict,
String startFrom) {
super(iteratorToRestrict);
this.startFrom = startFrom;
}
@Override
protected boolean isOk(String title) {
return title.compareTo(startFrom) >= 0;
}
}
/** requires title to start with given prefix */
private static class RestrictedIterator_Prefix
extends RestrictedIterator {
private String prefix;
/**
* constructor
* @param prefix String all titles must start with
*/
public RestrictedIterator_Prefix(Iterator<String> iteratorToRestrict,
String prefix) {
super(iteratorToRestrict);
this.prefix = prefix;
}
@Override
protected boolean isOk(String title) {
return title.startsWith(prefix);
}
}
/* Methods */
public MediaWikiText getInput(UIComponent uiForRequests) {
MediaWikiText result = null;
//check if there are still articles left, if yes, return the next one of them
if( titleIterator != null && titleIterator.hasNext() ){
String articleName = titleIterator.next();
result = readArticle(articleName);
} else { //if no articles are left
/* ask for input type (article, category ...),
* name of the article/cat./... and
* restrictions to selected articles
*/
String helpString = str("selectionHelp_Base");
// Is the prefix for image pages specified in the configuration file?
if (!imagePrefix.equals("")) {
helpString += str("selectionHelp_ImagePrefixYes",
imagePrefix);
} else if (!templatePrefix.equals("")) {
// No, but the template prefix is.
helpString += str("selectionHelp_ImagePrefixNo");
}
// Is the prefix for template pages specified in the configuration file?
if (!templatePrefix.equals("")) {
helpString += str("selectionHelp_TemplatePrefixYes",
templatePrefix);
} else if (!imagePrefix.equals("")) {
// Same here ...
helpString += str("selectionHelp_TemplatePrefixNo");
}
// Both prefixes are not specified
if (imagePrefix.equals("") && templatePrefix.equals("")) {
helpString += str("selectionHelp_BothNo");
}
helpString += str("selectionHelp_Restrictions");
List<UIRequest> requestList = new LinkedList<UIRequest>();
int selectedURLIndex = indexOfUrlInURLStringArray(conf_wikiURLs, currentURL);
if (selectedURLIndex == -1) {selectedURLIndex = 0;} //use first one by default
requestList.add(new UIRequest_Selection("url", str("mediaWikiRead"), str("helpMediaWikiRead"), selectedURLIndex, conf_wikiURLs));
requestList.add(new UIRequest_Selection("type", str("inputType"), str("helpInputType"), 0,
new String[]{str("inputSingleArticle"),
str("inputFrom"),
str("inputPrefix"),
str("inputCategory"),
str("inputLinkingTo"),
str("inputUsingImage"),
str("inputEmbeddingTemplate")}));
requestList.add(new UIRequest_String("name", str("inputName"), str("helpInputName")));
for (int i = 0; i < restrictionsInArticleSelection; i++ ) {
requestList.add(new UIRequest_Selection(
"restriction_type_" + i,
str("restrictionType", (i+1)),
str("helpRestrictionType"),
0,
new String[]{str("restrictionNone"),
str("inputFrom"),
str("inputPrefix")}));
requestList.add(new UIRequest_String(
"restriction_value_" + i,
str("restrictionValue", (i+1)),
str("helpRestrictionValue")));
}
requestList.add(new UIRequest_Output(helpString));
UIRequest[] requestArray = new UIRequest[requestList.size()];
requestArray = requestList.toArray(requestArray);
NamedDataSet reply = uiForRequests.request(requestArray);
if (reply == null) { return null; } //cancelled - #### !! return-point !! ####
try { //catch MalformedURLException and NamedDataNotAvailableException
createBot(conf_wikiURLs[reply.<Integer>get("url")]);
Integer type = reply.get("type");
String name = reply.get("name");
if ( type == 0 /*article*/ ){
result = readArticle(name);
}
else { //one of the iterator-generating types
try { //catch JWBFExceptions
/* get the appropriate iterator */
switch (type) {
case 1: /*articles from*/
titleIterator = new AllPageTitles(bot, name,null,nonredirects).iterator();
break;
case 2: /*articles starting with*/
titleIterator = new AllPageTitles(bot,null,name,nonredirects).iterator();
break;
case 3: /*category*/
titleIterator = new CategoryMembersSimple(bot, name).iterator();
break;
case 4: /*backlinks*/
titleIterator = new BacklinkTitles(bot, name).iterator();
break;
case 5: /*imagelinks*/
if (!imagePrefix.equals("")) {
name = imagePrefix + name;
}
titleIterator = new ImageUsageTitles(bot, name).iterator();
break;
case 6: /*template users*/
if (!templatePrefix.equals("")) {
name = templatePrefix + name;
}
titleIterator = new TemplateUserTitles(bot, name).iterator();
break;
}
/* apply the restrictions */
for (int i = 0; i < restrictionsInArticleSelection;
i ++) {
Integer restrictionType =
reply.get("restriction_type_" + i);
String restrictionValue =
reply.get("restriction_value_" + i);
if (restrictionType > 0 &&
restrictionValue != null) {
switch (restrictionType) {
case 1: /* articles from */
titleIterator =
new RestrictedIterator_StartFrom(
titleIterator,
restrictionValue);
break;
case 2: /* articles starting with */
titleIterator =
new RestrictedIterator_Prefix(
titleIterator,
restrictionValue);
break;
}
}
}
} catch (VersionException ve) {
String error = str("errorReceivingInfoWithExceptionName", "VersionException", ve.getMessage());
uiForRequests.request(new UIRequest_Output(error));
} catch (CookieException ce) {
String error = str("errorReceivingInfoWithExceptionName", "CookieException", ce.getMessage());
uiForRequests.request(new UIRequest_Output(error));
} catch (JwbfException je) {
String error = str("errorReceivingInfo", je.getMessage());
uiForRequests.request(new UIRequest_Output(error));
}
//return the first article
if (titleIterator.hasNext()) {
result = readArticle(titleIterator.next());
} else {
result = null;
}
}
} catch (MalformedURLException e) {
uiForRequests.request(
new UIRequest_Output(
str("errorMalformedURL") + e.getMessage() ));
} catch (NamedDataNotAvailableException e) {
uiForRequests.request(new UIRequest_Output(str("errorUserInput")));
}
}
/* optionally show read article in browser, then return text */
if (showArticleInBrowser && result != null) {
openArticleInBrowser(uiForRequests, result.getLabel());
}
return result;
}
/**
* tries to create a MediaWikiBot for the bot attribute.
* If the attempt succeeds, bot will be set to the new MediaWikiBot.
* Otherwise, bot will be set to null.
* The method also sets currentURL accordingly.
*
* @param wikiURL URL of the wiki the bot will work on
*/
private void createBot(String wikiURL) throws MalformedURLException {
URL url = new URL(wikiURL);
this.currentURL = url;
this.bot = new MediaWikiBot(url);
}
public void output(MediaWikiText text, UIComponent uiForRequests){
if (text == null) {return;}
/* show preview in browser if option selected */
/* TODO: use a button instead, requires changing the GUI concept */
if (showPreviewInBrowser) {
openPreviewInBrowser(uiForRequests, text);
}
/* create array of available wiki urls
* by adding this text's wiki url to the default list
*/
String[] wikiURLs = createExtendedWikiURLArray(text.getWikiURL());
/* show an overview before writing (the text can still be changed) */
if (showOverviewBeforeWriting) {
boolean completed = false;
while ( !completed ) {
int selectedURLIndex = indexOfUrlInURLStringArray(conf_wikiURLs, text.getWikiURL());
UIRequest[] overviewRequestArray = new UIRequest[] {
new UIRequest_Selection("url", str("mediaWikiWrite"), str("helpMediaWikiWrite"), selectedURLIndex, wikiURLs),
new UIRequest_String("label", str("articleName"), str("helpArticleName"), text.getLabel()),
new UIRequest_String("text", str("articleText"), str("helpArticleText"), text.getText(),multipleLines,lineWrap),
new UIRequest_String("summary", str("summary"), str("helpSummary"), text.getEditSummary()),
new UIRequest_Boolean("minor", str("minor"), str("helpMinor"), text.isMinorEdit())
};
NamedDataSet overviewReply = uiForRequests.request(overviewRequestArray);
if ( overviewReply == null ) {
text = null;
completed = true;
} else {
try {
createBot(conf_wikiURLs[overviewReply.<Integer>get("url")]);
text.setLabel(overviewReply.<String>get("label"));
text.setText(overviewReply.<String>get("text"));
text.setEditSummary(overviewReply.<String>get("summary"));
text.setMinorEdit(overviewReply.<Boolean>get("minor"));
completed = true;
} catch (MalformedURLException e) {
uiForRequests.request(
new UIRequest_Output(
str("errorMalformedURL", e.getMessage())));
} catch (NamedDataNotAvailableException e) {
uiForRequests.request(new UIRequest_Output(str("errorUserInput")));
}
}
}
} else {
if (text.getWikiURL() != null) {
try {
createBot(text.getWikiURL().toString());
} catch (MalformedURLException e) {
//do nothing, user will be asked
}
}
if (bot == null || alwaysAskForOutputWiki) {
int selectedURLIndex = indexOfUrlInURLStringArray(wikiURLs, text.getWikiURL());
UIRequest[] requestArray = new UIRequest[] {
new UIRequest_Selection("url", str("mediaWikiWrite"), str("helpMediaWikiWrite"), selectedURLIndex, conf_wikiURLs)
};
NamedDataSet reply = uiForRequests.request(requestArray);
try {
createBot(conf_wikiURLs[reply.<Integer>get("url")]);
} catch (MalformedURLException e) {
uiForRequests.request(
new UIRequest_Output(
str("errorMalformedURL", e.getMessage())));
} catch (NamedDataNotAvailableException e) {
uiForRequests.request(new UIRequest_Output(str("errorUserInput")));
}
}
}
/* write the article to the wiki (after logging in) */
if( text != null ){
/*
* if the bot is not logged in,
* but all information is available to log in, do so
*/
if( !bot.isLoggedIn()
&& username != null && !username.equals("")
&& password != null ){
try {
bot.login(username, password);
} catch (ActionException e) {
/* the "Login failed" message will be printed */
}
if ( !bot.isLoggedIn() && !password.equals("") ) {
uiForRequests.request(
new UIRequest_Output(str("errorLoginFailed")) );
}
}
/*
* if bot (still) not logged in, ask for username and password
* until the bot is logged in or the user gives up
*/
boolean tryAgain = true;
while( !bot.isLoggedIn() && tryAgain ){
UIRequest requestArray[] = {
new UIRequest_String("username", str("username"),
str("helpUsername"), username),
new UIRequest_String("password", str("password"),
str("helpPassword"),
UIRequest_String.LayoutFlag.hideInput)
};
NamedDataSet reply = uiForRequests.request(requestArray);
if ( reply == null ) { //cancelled
tryAgain = false;
}
else {
try {
username = reply.get("username");
password = reply.get("password");
try {
bot.login(username,password);
} catch (ActionException e) {
/* error message will be printed */
}
/* if login failed: ask about giving up */
if( bot.isLoggedIn() == false ){
UIRequest tryAgainRequestArray[] = { new UIRequest_Boolean("tryAgain", str("loginFailedTryAgain"), str("helpLoginFailedTryAgain")) };
try {
tryAgain = uiForRequests.request(tryAgainRequestArray).<Boolean>get("tryAgain");
} catch ( NamedDataNotAvailableException ndnae ){ tryAgain = false; /* no further attempts to log in */ }
}
} catch (NamedDataNotAvailableException ndnae) {
uiForRequests.request( new UIRequest_Output(str("errorGetAccountInfo")) );
username = null;
password = null;
}
}
}
/* after successful login: write the article to the MediaWiki */
if ( bot.isLoggedIn() ) {
boolean ready = false;
while ( !ready ) {
try {
writeArticle(text);
ready = true;
} catch ( ActionException ae ) {
/* writing failed, ready remains false */
} catch ( ProcessException pe ) {
/* writing failed, ready remains false */
}
if ( !ready ) {
UIRequest tryAgainRequestArray[] = { new UIRequest_Boolean("tryAgain", str("writingFailedTryAgain"), str("helpWritingFailedTryAgain")) };
try {
ready = (false == uiForRequests.request(tryAgainRequestArray).<Boolean>get("tryAgain"));
} catch ( NamedDataNotAvailableException ndnae ) {
System.err.println(str("exceptionUserInput", ndnae.toString()));
ready = true; /* no further attempts to write */
}
}
}
}
}
}
/**
* returns the last index of a string in ss that equals an url's string rep
* or -1 if s == null or s not in ss
*/
private static final int indexOfUrlInURLStringArray(String[] ss, URL url) {
assert ss != null;
if (url == null) {
return -1;
}
int index = -1;
String urlString = url.toString();
for (int i = 0; i < ss.length; i++) {
if (ss[i].equals(urlString)) {
index = i;
}
}
return index;
}
/**
* creates an array of available wiki urls
* by adding this text's wiki url to the conf_wikiURLs
* if additionalURL is null, simply returns the conf_wikiURLs.
*/
private final String[] createExtendedWikiURLArray(URL additionalURL) {
if (additionalURL == null) {
return conf_wikiURLs;
}
String addURLString = additionalURL.toString();
boolean arrayContainsURL = false;
for (String arrayURL : conf_wikiURLs) {
if (arrayURL.equals(addURLString)) {
arrayContainsURL = true;
break;
}
}
if (arrayContainsURL) {
return conf_wikiURLs;
} else {
String[] extendedArray = new String[conf_wikiURLs.length + 1];
extendedArray = Arrays.copyOf(conf_wikiURLs, conf_wikiURLs.length + 1);
extendedArray[conf_wikiURLs.length] = addURLString;
return extendedArray;
}
}
/**
* uses the bot to get a Text object representing a MW article,
* catches all exceptions.
*
* @param name article name
* @return PlainText object representing article
* or null if exceptions occurred
*/
private MediaWikiText readArticle(String name) {
ContentAccessable content;
try {
content = bot.readContent(name);
} catch (ActionException e) {
return null;
} catch (ProcessException e) {
return null;
}
if ( content == null ) {
return null;
} else {
return new MediaWikiText(
content.getText(),
content.getTitle(),
"",
true,
currentURL);
}
}
/**
* uses the bot to write an article to the MediaWiki
*
* @throws ProcessException
* @throws ActionException
*/
private void writeArticle(MediaWikiText text) throws ActionException, ProcessException {
SimpleArticle article = new SimpleArticle();
article.setTitle(text.getLabel());
article.setText(text.getText());
article.setMinorEdit(text.isMinorEdit());
article.setEditSummary(text.getEditSummary());
bot.writeContent(article);
}
/**
* opens the article with the given label (in current wiki) in the browser.
* currentURL must not be null.
*
* @param label label of the article to open in browser
* @param uiForRequests the ui for error messages
*/
private void openArticleInBrowser(UIComponent uiForRequests, String label) {
assert currentURL != null;
String url = currentURL + "?action=view&title=" + encodeLabel(label);
openURLInBrowser(uiForRequests, url);
}
/**
* opens a preview for the given article in the browser
*
* @param text the article to show a preview page for
* @param uiForRequests the ui for error messages
*/
private void openPreviewInBrowser(UIComponent uiForRequests, MediaWikiText text) {
//TODO: implement
}
/**
* encodes a label by
* 1. replacing all spaces with '_'
* 2. performing a default url-encode operation
*
* @param label label for encoding
* @return encoded label
*/
private String encodeLabel(String label) {
String encodedLabel = label.replaceAll(" ","_");
try {
encodedLabel = URLEncoder.encode(encodedLabel, "UTF-8");
} catch (UnsupportedEncodingException e) {
/* note that this should not be possible. Java 5.0 API states:
* "every implementation of the Java platform is required to support
* the following standard charsets. [...] UTF-8 [...]"
*/
throw new Error(str("errorLabelEncoding"));
}
return encodedLabel;
}
/**
* opens an URL in the browser using the existing browser launcher
* (or creating a new one if necessary). Prints error messages to ui.
*
* @param url url to open in browser, not null
* @param uiForRequests the ui for error messages, not null
*/
private void openURLInBrowser(UIComponent uiForRequests, String url) {
assert uiForRequests != null && url != null;
if (browserLauncher == null) {
try {
browserLauncher = new BrowserLauncher();
} catch (BrowserLaunchingInitializingException e) {
uiForRequests.request(new UIRequest_Output(str("errorBrowser")));
} catch (UnsupportedOperatingSystemException e) {
uiForRequests.request(
new UIRequest_Output(str("errorBrowserOS")));
}
}
browserLauncher.openURLinBrowser(url);
}
public boolean hasNext() {
return titleIterator != null && titleIterator.hasNext();
}
@Override
public void handleConfigurationUpdate() {
//set log4j-configuration file (used in JWBF)
PropertyConfigurator.configureAndWatch(
log4jConfigurationPath, 60*1000);
}
}