/** * Copyright (C) 2010 Peter Karich <jetwick_@_pannous_._info> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.jetwick.ui; import de.jetwick.tw.MyTweetGrabber; import com.google.inject.Inject; import com.google.inject.Provider; import de.jetwick.data.UrlEntry; import de.jetwick.es.ElasticTweetSearch; import de.jetwick.es.JetwickQuery; import de.jetwick.es.SavedSearch; import de.jetwick.es.SimilarTweetQuery; import de.jetwick.data.JTweet; import de.jetwick.data.JUser; import de.jetwick.es.TweetQuery; import de.jetwick.tw.TwitterSearch; import de.jetwick.tw.queue.QueueThread; import de.jetwick.ui.jschart.JSDateFilter; import de.jetwick.util.Helper; import de.jetwick.util.StopWatch; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import org.apache.wicket.PageParameters; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.model.Model; import org.apache.wicket.protocol.http.WebResponse; import org.apache.wicket.request.target.basic.RedirectRequestTarget; import org.elasticsearch.action.search.SearchResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import twitter4j.TwitterException; /** * TODO clean up this bloated class * @author Peter Karich, peat_hal 'at' users 'dot' sourceforge 'dot' net */ public class TweetSearchPage extends JetwickPage { private static final long serialVersionUID = 1L; public static final String Q = "query"; public static final String CRAWLER = "crawler"; public static final String PASSWORD = "pw"; public static final String PASSWORD_CORRECT = "geeknews"; public static final String TIME = "time"; public static final String TIME_TODAY = "today"; private final Logger logger = LoggerFactory.getLogger(getClass()); private JetwickQuery lastQuery; private int hitsPerPage = 15; private FeedbackPanel feedbackPanel; private ResultsPanel resultsPanel; private FacetPanel facetPanel; private SavedSearchPanel ssPanel; private TagCloudPanel tagCloudPanel; private NavigationPanel navigationPanel; private SearchBox searchBox; private String language = "en"; private String remoteHost = ""; // private WikipediaLazyLoadPanel wikiPanel; private UrlTrendPanel urlTrends; @Inject private Provider<ElasticTweetSearch> twindexProvider; @Inject private MyTweetGrabber grabber; private boolean twitterFallback = false; private JSDateFilter dateFilterPanel; private transient Thread tweetThread; private static int TWEETS_IF_HIT = 30; private static int TWEETS_IF_NO_HIT = 40; private String userName = ""; // for testing TweetSearchPage() { } TweetSearchPage(JetwickQuery q) { init(q, PageParameters.NULL); } public TweetSearchPage(final PageParameters parameters) { super.init(parameters); } @Override protected void configureResponse() { super.configureResponse(); super.myConfigureResponse(); } public ElasticTweetSearch getTweetSearch() { return twindexProvider.get(); } public Thread getQueueThread() { return tweetThread; } @Override public JetwickQuery createQuery(PageParameters parameters) { // TODO M2.1 parameters.get("hits").toString can cause NPE!! String hitsStr = parameters.getString("hits"); if (hitsStr != null) { try { hitsPerPage = Integer.parseInt(hitsStr); hitsPerPage = Math.min(100, hitsPerPage); } catch (Exception ex) { logger.warn("Couldn't parse hits per page:" + hitsStr + " " + ex.getMessage()); } } String idStr = parameters.getString("id"); JetwickQuery q = null; if (idStr != null) { try { int index = idStr.lastIndexOf("/"); if (index > 0 && index + 1 < idStr.length()) idStr = idStr.substring(index + 1); q = new TweetQuery(true).addFilterQuery(ElasticTweetSearch._ID + "tweet", Long.parseLong(idStr)); } catch (Exception ignore) { } } if (q == null) { String originStr = parameters.getString("findOrigin"); if (originStr != null) { logger.info("[stats] findOrigin from lastQuery:" + lastQuery); q = getTweetSearch().createFindOriginQuery(lastQuery, originStr, 3); } } String queryStr = parameters.getString("q"); if(Helper.isEmpty(queryStr)) queryStr = parameters.getString("query"); if (queryStr == null) queryStr = ""; if(queryStr.contains("*")) { warn("Cannot process query with asterisk"); queryStr = ""; } userName = ""; if (q == null) { userName = parameters.getString("u"); if (userName == null) userName = parameters.getString("user"); if (userName == null) userName = ""; q = new TweetQuery(queryStr); if (!Helper.isEmpty(userName)) q.addUserFilter(userName); } String fromDateStr = parameters.getString("until"); if (fromDateStr != null) { if (!fromDateStr.contains("T")) fromDateStr += "T00:00:00Z"; q.addFilterQuery(ElasticTweetSearch.DATE, "[" + fromDateStr + " TO *]"); } // front page/empty => sort against relevance // user search => sort against latest date // other => sort against retweets if no sort specified String sort = parameters.getString("sort"); if ("retweets".equals(sort)) q.setSort(ElasticTweetSearch.RT_COUNT, "desc"); else if ("latest".equals(sort)) q.setSort(ElasticTweetSearch.DATE, "desc"); else if ("oldest".equals(sort)) q.setSort(ElasticTweetSearch.DATE, "asc"); else if ("relevance".equals(sort)) q.setSort(ElasticTweetSearch.RELEVANCE, "desc"); else { q.setSort(ElasticTweetSearch.RT_COUNT, "desc"); if (!Helper.isEmpty(userName)) q.setSort(ElasticTweetSearch.DATE, "desc"); } // front page: avoid slow queries for matchall query and filter against latest tweets only if (queryStr.isEmpty() && q.getFilterQueries().isEmpty() && fromDateStr == null) { logger.info(addIP("[stats] q=''")); q.addLatestDateFilter(8); if (Helper.isEmpty(sort)) q.setSort(ElasticTweetSearch.RELEVANCE, "desc"); } String filter = parameters.getString("filter"); if (Helper.isEmpty(userName) && !"none".equals(filter)) { q.addNoSpamFilter().addNoDupsFilter().addIsOriginalTweetFilter().addLatestDateFilter(24 * 7); } return q; } public void updateAfterAjax(AjaxRequestTarget target, boolean updateSearchBox) { if (target != null) { target.addComponent(facetPanel); target.addComponent(resultsPanel); target.addComponent(navigationPanel); //already in resultsPanel target.addComponent(lazyLoadAdPanel); if (updateSearchBox) target.addComponent(searchBox); target.addComponent(tagCloudPanel); target.addComponent(dateFilterPanel); target.addComponent(urlTrends); target.addComponent(feedbackPanel); target.addComponent(ssPanel); // no ajax for wikipedia to avoid requests //target.addComponent(wikiPanel); // this does not work (scroll to top) // target.focusComponent(searchBox); } } public void setTwitterFallback(boolean twitterFallback) { this.twitterFallback = twitterFallback; } @Override public void init(JetwickQuery query, final PageParameters parameters) { feedbackPanel = new FeedbackPanel("feedback"); add(feedbackPanel.setOutputMarkupId(true)); add(new Label("title", new Model() { @Override public Serializable getObject() { String str = ""; if (!searchBox.getQuery().isEmpty()) str += searchBox.getQuery() + " "; if (!searchBox.getUserName().isEmpty()) { if (str.isEmpty()) str = "User " + searchBox.getUserName() + " "; else str = "Search " + str + "in user " + searchBox.getUserName() + " "; } if (str.isEmpty()) return "Jetwick Twitter Search"; return "Jetwick | " + str + "| Twitter Search Without Noise"; } })); add(new ExternalLinksPanel("externalRefs")); add(new ExternalLinksPanelRight("externalRefsRight")); urlTrends = new UrlTrendPanel("urltrends") { @Override protected void onUrlClick(AjaxRequestTarget target, String name) { JetwickQuery q; if (lastQuery != null) q = lastQuery; else q = new TweetQuery(true); if (name == null) { q.removeFilterQueries(ElasticTweetSearch.FIRST_URL_TITLE); } else q.addFilterQuery(ElasticTweetSearch.FIRST_URL_TITLE, name); doSearch(q, 0, true); updateAfterAjax(target, false); } @Override protected void onDirectUrlClick(AjaxRequestTarget target, String name) { if (lastQuery == null || name == null || name.isEmpty()) return; TweetQuery q = new TweetQuery(true); q.addFilterQuery(ElasticTweetSearch.FIRST_URL_TITLE, name); try { List<JTweet> tweets = getTweetSearch().collectObjects(getTweetSearch().query(q.setSize(1))); if (tweets.size() > 0 && tweets.get(0).getUrlEntries().size() > 0) { // TODO there could be more than 1 url! UrlEntry entry = tweets.get(0).getUrlEntries().iterator().next(); getRequestCycle().setRequestTarget(new RedirectRequestTarget(entry.getResolvedUrl())); } } catch (Exception ex) { logger.error("Error while executing onDirectUrlClick", ex); } } }; add(urlTrends.setOutputMarkupId(true)); ssPanel = new SavedSearchPanel("savedSearches") { @Override public void onClick(AjaxRequestTarget target, long ssId) { String searchType = parameters.getString("search"); if (searchType != null && !searchType.isEmpty() && !SearchBox.ALL.equals(searchType)) { warn("Removed user filter when executing your saved search"); searchBox.setSearchType(SearchBox.ALL); } JUser user = getMySession().getUser(); SavedSearch ss = user.getSavedSearch(ssId); if (ss != null) { doSearch(ss.getQuery(), 0, true); uindexProvider.get().save(user, true); } updateSSCounts(target); updateAfterAjax(target, true); } @Override public void onRemove(AjaxRequestTarget target, long ssId) { JUser user = getMySession().getUser(); user.removeSavedSearch(ssId); uindexProvider.get().save(user, true); updateSSCounts(target); } @Override public void onSave(AjaxRequestTarget target, long ssId) { if (lastQuery == null) return; SavedSearch ss = new SavedSearch(ssId, lastQuery); JUser user = getMySession().getUser(); user.addSavedSearch(ss); uindexProvider.get().save(user, true); updateSSCounts(target); } @Override public void updateSSCounts(AjaxRequestTarget target) { try { JUser user = getMySession().getUser(); if (user != null) { StopWatch sw = new StopWatch().start(); update(getTweetSearch().updateSavedSearches(user.getSavedSearches())); if (target != null) target.addComponent(ssPanel); logger.info("Updated saved search counts for " + user.getScreenName() + " " + sw.stop().getSeconds()); } } catch (Exception ex) { logger.error("Error while searching in savedSearches", ex); } } @Override public String translate(long id) { SavedSearch ss = getMySession().getUser().getSavedSearch(id); return ss.getName(); } }; add(ssPanel.setOutputMarkupId(true)); add(new UserPanel("userPanel", this) { @Override public void onLogout() { getMySession().logout(uindexProvider.get(), (WebResponse) getResponse(), true); setResponsePage(TweetSearchPage.class, parameters); } @Override public void updateAfterAjax(AjaxRequestTarget target) { TweetSearchPage.this.updateAfterAjax(target, false); } @Override public void onHomeline(AjaxRequestTarget target, String user) { searchBox.setSearchType(SearchBox.FRIENDS); doSearch(createFriendQuery(""), 0, false); TweetSearchPage.this.updateAfterAjax(target, true); } @Override public void onShowTweets(AjaxRequestTarget target, String userName) { searchBox.setSearchType(SearchBox.USER); doSearch(new TweetQuery(true).addUserFilter(userName).setSort(ElasticTweetSearch.DATE, "desc"), 0, false); TweetSearchPage.this.updateAfterAjax(target, true); } @Override protected Collection<String> getUserChoices(String input) { return getTweetSearch().getUserChoices(lastQuery, input); } }); tagCloudPanel = new TagCloudPanel("tagcloud") { @Override protected void onTagClick(String name) { if (lastQuery != null) { lastQuery.setQuery((lastQuery.getQuery() + " " + name).trim()); doSearch(lastQuery, 0, true); } else { // never happens? PageParameters pp = new PageParameters(); pp.add("q", name); setResponsePage(TweetSearchPage.class, pp); } } @Override protected void onFindOriginClick(String tag) { PageParameters pp = new PageParameters(); pp.add("findOrigin", tag); doSearch(createQuery(pp), 0, true); // this preserves parameters but cannot be context sensitive! // setResponsePage(TweetSearchPage.class, pp); } }; add(tagCloudPanel.setOutputMarkupId(true)); navigationPanel = new NavigationPanel("navigation", hitsPerPage) { @Override public void onPageChange(AjaxRequestTarget target, int page) { // this does not scroll to top: // doOldSearch(page); // updateAfterAjax(target); doOldSearch(page); } }; add(navigationPanel.setOutputMarkupId(true)); facetPanel = new FacetPanel("filterPanel") { @Override public void onRemoveAllFilter(AjaxRequestTarget target, String key) { if (lastQuery != null) lastQuery.removeFilterQueries(key); else { logger.error("last query cannot be null but was! ... when clicking on facets!?"); return; } doOldSearch(0); updateAfterAjax(target, false); } @Override public void onFilterChange(AjaxRequestTarget target, String key, Object val, Boolean selected) { if (lastQuery != null) { if (selected == null) lastQuery.removeFilterQuery(key, val); else if (selected) lastQuery.addFilterQuery("-" + key, val); else lastQuery.addFilterQuery(key, val); } else { logger.error("last query cannot be null but was! ... when clicking on facets!?"); return; } doOldSearch(0); updateAfterAjax(target, false); } }; add(facetPanel.setOutputMarkupId(true)); dateFilterPanel = new JSDateFilter("dateFilter") { @Override protected void onFilterChange(AjaxRequestTarget target, String filter, Boolean selected) { if (lastQuery != null) { if (selected == null) { lastQuery.removeFilterQueries(filter); } else if (selected) { lastQuery.replaceFilterQuery(filter); } else lastQuery.reduceFilterQuery(filter); } else { logger.error("last query cannot be null but was! ... when clicking on facets!?"); return; } doOldSearch(0); updateAfterAjax(target, false); } @Override protected boolean isAlreadyFiltered(String key, Object val) { if (lastQuery != null) return lastQuery.containsFilter(key, val); return false; } @Override public String getFilterName(String key) { return facetPanel.getFilterName(key); } }; add(dateFilterPanel.setOutputMarkupId(true)); // TODO M2.1 language = getWebRequestCycle().getWebRequest().getHttpServletRequest().getLocale().getLanguage(); remoteHost = getWebRequestCycle().getWebRequest().getHttpServletRequest().getRemoteHost(); resultsPanel = new ResultsPanel("results", language) { @Override public void onSortClicked(AjaxRequestTarget target, String sortKey, String sortVal) { if (lastQuery != null) { lastQuery.setSort(sortKey, sortVal); doSearch(lastQuery, 0, false); updateAfterAjax(target, false); } } @Override public void onUserClick(String userName, String queryStr) { PageParameters p = new PageParameters(); if (queryStr != null && !queryStr.isEmpty()) p.add("q", queryStr); if (userName != null) { p.add("user", userName.trim()); searchBox.setSearchType(SearchBox.USER); } doSearch(createQuery(p), 0, true); } @Override public Collection<JTweet> onTweetClick(long id, boolean retweet) { logger.info("[stats] search replies of:" + id + " retweet:" + retweet); return getTweetSearch().searchReplies(id, retweet); } @Override protected void onRetweet(JTweet tweet, AjaxRequestTarget target) { if (getMySession().hasLoggedIn()) try { getTwitterSearch().doRetweet(tweet.getTwitterId()); info("Retweeted " + tweet.getFromUser().getScreenName() + " by " + getTwitterSearch().getUser()); } catch (Exception ex) { error("Cannot retweet " + tweet.getFromUser().getScreenName() + ". " + "Problems with twitter. Please try again."); } else error("Please login."); updateAfterAjax(target, false); } @Override public void onFindSimilar(JTweet tweet, AjaxRequestTarget target) { JetwickQuery query = new SimilarTweetQuery(tweet, true); if (tweet.getTextTerms().size() == 0) { warn("Try a different tweet. This tweet is too short."); return; } logger.info("[stats] similar search:" + query.toString()); doSearch(query, 0, false); updateAfterAjax(target, false); } @Override public Collection<JTweet> onInReplyOfClick(long id) { JTweet tw = getTweetSearch().findByTwitterId(id); logger.info("[stats] search tweet:" + id + " " + tw); if (tw != null) return Arrays.asList(tw); else return new ArrayList(); } @Override public String getTweetsAsString() { if (lastQuery != null) return twindexProvider.get().getTweetsAsString(lastQuery, "\t"); return ""; } @Override public void onHtmlExport() { if (lastQuery != null) { PrinterPage printerPage = new PrinterPage(); List<JTweet> tweets = twindexProvider.get().searchTweets(lastQuery); printerPage.setResults(tweets); setResponsePage(printerPage); } } }; add(resultsPanel.setOutputMarkupId(true)); // add(wikiPanel = new WikipediaLazyLoadPanel("wikipanel")); String searchType = parameters.getString("search"); String tmpUserName = null; boolean showSpacer = true; if (getMySession().hasLoggedIn()) { if (query.getQuery().isEmpty() && userName.isEmpty() && (searchType == null || searchType.isEmpty())) searchType = SearchBox.FRIENDS; tmpUserName = getMySession().getUser().getScreenName(); // showSpacer = false; } else { ssPanel.setVisible(false); facetPanel.setVisible(false); // dateFilterPanel.setVisible(false); // so that the jetwick link on my twitter account works ;) if (userName.isEmpty()) { tagCloudPanel.setVisible(false); // if (query.getQuery().isEmpty()) { // resultsPanel.setVisible(false); // navigationPanel.setVisible(false); // showSpacer = false; // } } } dateFilterPanel.setVisible(false); searchBox = new SearchBox("searchbox", tmpUserName, searchType, showSpacer) { @Override protected Collection<String> getQueryChoices(String input) { return getTweetSearch().getQueryChoices(lastQuery, input); } @Override protected void onSelectionChange(AjaxRequestTarget target, String newValue) { if (lastQuery == null) return; JetwickQuery tmpQ = lastQuery.getCopy().setQuery(newValue); tmpQ.removeFilterQueries(ElasticTweetSearch.DATE); doSearch(tmpQ, 0, false, true); updateAfterAjax(target, false); } @Override protected Collection<String> getUserChoices(String input) { return getTweetSearch().getUserChoices(lastQuery, input); } }; add(searchBox.setOutputMarkupId(true)); if (SearchBox.FRIENDS.equalsIgnoreCase(searchType)) { twitterFallback = false; query = createFriendQuery(query.getQuery()); if (query == null) return; } doSearch(query, 0, twitterFallback); } public JetwickQuery createFriendQuery(String queryStr) { if (getMySession().hasLoggedIn()) { Collection<String> friends = getMySession().getFriends(uindexProvider.get()); if (friends.isEmpty()) { info("You recently logged in. Please try again in 2 minutes to use friend search."); } else { return new TweetQuery(queryStr).createFriendsQuery(friends). addLatestDateFilter(8). setSort(ElasticTweetSearch.DATE, "desc"); } } else info("Login to use friend search!"); PageParameters pp = new PageParameters(); pp.put("q", queryStr); return createQuery(pp); } /** * used from facets (which adds filter queries) and * from footer which changes the page */ public void doOldSearch(int page) { logger.info(addIP("[stats] change old search. page:" + page)); if (lastQuery == null) { lastQuery = createQuery(new PageParameters()); logger.warn(addIP("Last query is null!? created new default")); } doSearch(lastQuery, page, false); } public void doSearch(JetwickQuery query, int page, boolean twitterFallback) { doSearch(query, page, twitterFallback, false); } public void doSearch(JetwickQuery query, int page, boolean twitterFallback, boolean instantSearch) { if (getMySession().hasLoggedIn()) query.attachUserFacets(); String queryString; if (!instantSearch) { // change text field searchBox.init(query.setEscape(false).getQuery(), query.extractUserName()); } queryString = query.setEscape(true).getQuery(); // if query is lastQuery then user is saved in filter not in a pageParam String userName = searchBox.getUserName(); // do not show front page // if (!getMySession().hasLoggedIn() && query.getQuery().isEmpty() && userName.isEmpty()) // return; // wikiPanel.setParams(queryString, language); boolean startBGThread = true; // do not trigger background searchAndGetUsers if this query is the identical // to the last searchAndGetUsers or if it is an instant searchAndGetUsers if (instantSearch || lastQuery != null && queryString.equals(lastQuery.getQuery()) && userName.equals(lastQuery.extractUserName())) startBGThread = false; // do not trigger twitter searchAndGetUsers if a searchAndGetUsers through a users' tweets is triggered if (!userName.isEmpty() && !queryString.isEmpty()) twitterFallback = false; if (query.containsFilterKey("id")) twitterFallback = false; if (!instantSearch) lastQuery = query; Collection<JUser> users = new LinkedHashSet<JUser>(); query.attachPagability(page, hitsPerPage); long start = System.currentTimeMillis(); long totalHits = 0; SearchResponse rsp = null; try { rsp = getTweetSearch().query(users, query); totalHits = rsp.getHits().getTotalHits(); logger.info(addIP("[stats] " + totalHits + " hits for: " + query.toString())); } catch (Exception ex) { logger.error("Error while searching " + query.toString(), ex); } resultsPanel.clear(); Collection<JTweet> tweets = null; String msg = ""; if (totalHits > 0) { float time = (System.currentTimeMillis() - start) / 100.0f; time = Math.round(time) / 10f; msg = "Found " + totalHits + " tweets in " + time + " s"; } else { if (queryString.isEmpty()) { if (userName.isEmpty()) { // something is wrong with our index because q='' and user='' should give us all docs! logger.warn(addIP("[stats] 0 results for q='' using news!")); queryString = "news"; startBGThread = false; } else msg = "for user '" + userName + "'"; } if (twitterFallback) { // try TWITTER SEARCH users.clear(); try { if (getTwitterSearch().getRateLimitFromCache() > TwitterSearch.LIMIT) { if (!userName.isEmpty()) { tweets = getTwitterSearch().getTweets(new JUser(userName), users, TWEETS_IF_NO_HIT); } else tweets = getTwitterSearch().searchAndGetUsers(queryString, users, TWEETS_IF_NO_HIT, 1); } } catch (TwitterException ex) { logger.warn("Warning while querying twitter:" + ex.getMessage()); } catch (Exception ex) { logger.error("Error while querying twitter:" + ex.getMessage()); } } if (users.isEmpty()) { if (!msg.isEmpty()) msg = " " + msg; msg = "Sorry, nothing found" + msg + "."; } else { resultsPanel.setQueryMessageWarn("Sparse results."); msg = "Using twitter-search " + msg + "."; logger.warn("[stats] qNotFound:" + query.getQuery()); } if (startBGThread) msg += " Please try again in two minutes to get jetwicked results."; } if (startBGThread) { try { tweetThread = new Thread(queueTweets(tweets, queryString, userName)); tweetThread.start(); } catch (Exception ex) { logger.error("Couldn't queue tweets. query " + queryString + " user=" + userName + " Error:" + Helper.getMsg(ex)); } } else tweetThread = null; facetPanel.update(rsp, query); tagCloudPanel.update(rsp, query); urlTrends.update(rsp, query); resultsPanel.setQueryMessage(msg); resultsPanel.setQuery(queryString); resultsPanel.setUser(userName); resultsPanel.setHitsPerPage(hitsPerPage); dateFilterPanel.update(rsp); if (!query.getSortFields().isEmpty()) { resultsPanel.setSort(query.getSortFields().get(0).getKey(), query.getSortFields().get(0).getValue()); } else resultsPanel.setSort(null, null); resultsPanel.setTweetsPerUser(-1); for (JUser user : users) { resultsPanel.add(user); } navigationPanel.setPage(page); navigationPanel.setHits(totalHits); navigationPanel.setHitsPerPage(hitsPerPage); navigationPanel.updateVisibility(); logger.info(addIP("Finished Constructing UI.")); } public QueueThread queueTweets(Collection<JTweet> tweets, String qs, String userName) { return grabber.init(tweets, qs, userName).setTweetsCount(TWEETS_IF_HIT). setTwitterSearch(getTwitterSearch()).queueTweetPackage(); } String addIP(String str) { String q = ""; if (getWebRequestCycle() != null) q = getWebRequestCycle().getWebRequest().getParameter("q"); return str + " IP=" + remoteHost + " session=" + getSession().getId() + " q=" + q; } }