/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.contrib.mailarchive.timeline.internal;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.jfree.util.Log;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.context.Execution;
import org.xwiki.context.ExecutionContext;
import org.xwiki.contrib.mailarchive.IMAUser;
import org.xwiki.contrib.mailarchive.IMailArchiveConfiguration;
import org.xwiki.contrib.mailarchive.IMailingList;
import org.xwiki.contrib.mailarchive.IType;
import org.xwiki.contrib.mailarchive.exceptions.MailArchiveException;
import org.xwiki.contrib.mailarchive.timeline.ITimeLineGenerator;
import org.xwiki.contrib.mailarchive.utils.DecodedMailContent;
import org.xwiki.contrib.mailarchive.utils.IMailUtils;
import org.xwiki.contrib.mailarchive.utils.ITextUtils;
import org.xwiki.contrib.mailarchive.xwiki.internal.XWikiPersistence;
import org.xwiki.model.reference.DocumentReferenceResolver;
import org.xwiki.query.Query;
import org.xwiki.query.QueryException;
import org.xwiki.query.QueryManager;
import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter;
import com.xpn.xwiki.XWiki;
import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.XWikiException;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.objects.BaseObject;
/**
* @version $Id$
*/
@Component
@Singleton
public class TimeLineGenerator implements Initializable, ITimeLineGenerator
{
public static final int DEFAULT_MAX_ITEMS = 200;
@Inject
private IMailArchiveConfiguration config;
@Inject
private Logger logger;
@Inject
private QueryManager queryManager;
/** Provides access to the request context. */
@Inject
public Execution execution;
private XWiki xwiki;
private XWikiContext context;
@Inject
private DocumentReferenceResolver<String> docResolver;
@Inject
@Named("simile")
ITimeLineDataWriter timelineWriter;
@Inject
private IMailUtils mailUtils;
@Inject
private ITextUtils textUtils;
private String userStatsUrl = "";
/**
* {@inheritDoc}
*
* @see org.xwiki.component.phase.Initializable#initialize()
*/
@Override
public void initialize() throws InitializationException
{
ExecutionContext context = execution.getContext();
this.context = (XWikiContext) context.getProperty("xwikicontext");
this.xwiki = this.context.getWiki();
}
public String compute()
{
return compute(Integer.MAX_VALUE);
}
/**
* {@inheritDoc}
*
* @see org.xwiki.contrib.mailarchive.timeline.ITimeLineGenerator#compute()
*/
@Override
public String compute(int maxItems)
{
try {
config.reloadConfiguration();
} catch (MailArchiveException e) {
logger.error("Could not load mail archive configuration", e);
return null;
}
Map<String, IMailingList> mailingLists = config.getMailingLists();
Map<String, IType> types = config.getMailTypes();
try {
this.userStatsUrl =
xwiki.getDocument(docResolver.resolve("MailArchive.ViewUserMessages"), context).getURL("view", context);
} catch (XWikiException e1) {
logger.warn("Could not retrieve user stats url {}", ExceptionUtils.getRootCauseMessage(e1));
}
TreeMap<Long, TimeLineEvent> sortedEvents = new TreeMap<Long, TimeLineEvent>();
// Set loading user in context (for rights)
String loadingUser = config.getLoadingUser();
context.setUserReference(docResolver.resolve(loadingUser));
try {
// Search topics
String xwql =
"select doc.fullName, topic.author, topic.subject, topic.topicid, topic.startdate, topic.lastupdatedate from Document doc, doc.object("
+ XWikiPersistence.CLASS_TOPICS + ") as topic order by topic.lastupdatedate desc";
List<Object[]> result = queryManager.createQuery(xwql, Query.XWQL).setLimit(maxItems).execute();
for (Object[] item : result) {
XWikiDocument doc = null;
try {
String docurl = (String) item[0];
String author = (String) item[1];
String subject = (String) item[2];
String topicId = (String) item[3];
Date date = (Date) item[4];
Date end = (Date) item[5];
String action = "";
// Retrieve associated emails
TreeMap<Long, TopicEventBubble> emails = getTopicMails(topicId, subject);
if (emails == null || emails.isEmpty()) {
// Invalid topic, not emails attached, do not show it
logger.warn("Invalid topic, no emails attached " + doc);
} else {
if (date != null && end != null && date.equals(end)) {
// Add 10 min just to see the tape
end.setTime(end.getTime() + 600000);
}
doc = xwiki.getDocument(docResolver.resolve(docurl), context);
final List<String> tagsList = doc.getTagsList(context);
List<String> topicTypes = doc.getListValue(XWikiPersistence.CLASS_TOPICS, "type");
// Email type icon
List<String> icons = new ArrayList<String>();
for (String topicType : topicTypes) {
IType type = types.get(topicType);
if (type != null && !StringUtils.isEmpty(type.getIcon())) {
icons.add(xwiki.getSkinFile("icons/silk/" + type.getIcon() + ".png", context));
// http://localhost:8080/xwiki/skins/colibri/icons/silk/bell
// http://localhost:8080/xwiki/resources/icons/silk/bell.png
}
}
// Author and avatar
final IMAUser wikiUser = mailUtils.parseUser(author, config.isMatchLdap());
final String authorAvatar = getAuthorAvatar(wikiUser.getWikiProfile());
final TimeLineEvent timelineEvent = new TimeLineEvent();
TimeLineEvent additionalEvent = null;
timelineEvent.beginDate = date;
timelineEvent.title = subject;
timelineEvent.icons = icons;
timelineEvent.lists = doc.getListValue(XWikiPersistence.CLASS_TOPICS, "list");
timelineEvent.author = wikiUser.getDisplayName();
timelineEvent.authorAvatar = authorAvatar;
timelineEvent.extract = getExtract(topicId);
if (emails.size() == 1) {
logger.debug("Adding instant event for email '" + subject + "'");
// Unique email, we show a punctual email event
timelineEvent.url = emails.firstEntry().getValue().link;
timelineEvent.action = "New Email ";
} else {
// For email with specific type icon, and a duration, both a band and a point should be added (so 2 events)
// because no icon is displayed for duration events.
if (CollectionUtils.isNotEmpty(icons)) {
logger.debug("Adding additional instant event to display type icon for first email in topic");
additionalEvent = new TimeLineEvent(timelineEvent);
additionalEvent.url = emails.firstEntry().getValue().link;
additionalEvent.action = "New Email ";
}
// Email thread, we show a topic event (a range)
logger.debug("Adding duration event for topic '" + subject + "'");
timelineEvent.endDate = end;
timelineEvent.url = doc.getURL("view", context);
timelineEvent.action = "New Topic ";
timelineEvent.messages = emails;
}
// Add the generated Event to the list
if (sortedEvents.containsKey(date.getTime())) {
// Avoid having more than 1 event at exactly the same time, because some timeline don't like it
date.setTime(date.getTime() + 1);
}
sortedEvents.put(date.getTime(), timelineEvent);
if (additionalEvent != null) {
sortedEvents.put(date.getTime() + 1, additionalEvent);
}
}
} catch (Throwable t) {
logger.warn("Exception for " + doc, t);
}
}
} catch (Throwable e) {
logger.warn("could not compute timeline data", e);
}
return printEvents(sortedEvents);
}
public String toString(TreeMap<Long, TimeLineEvent> sortedEvents)
{
return printEvents(sortedEvents);
}
private String printEvents(TreeMap<Long, TimeLineEvent> sortedEvents)
{
DefaultWikiPrinter printer = new DefaultWikiPrinter();
timelineWriter.setWikiPrinter(printer);
timelineWriter.print(sortedEvents);
logger.debug("Loaded " + sortedEvents.size() + " into Timeline feed");
logger.debug("Timeline data {}", printer.toString());
return printer.toString();
}
/**
* Formats a timeline bubble content, ie html presenting list of mails related to a given topic.
*
* @param topicid
* @param topicsubject
* @return
* @throws QueryException
* @throws XWikiException
*/
protected TreeMap<Long, TopicEventBubble> getTopicMails(String topicid, String topicsubject) throws QueryException,
XWikiException
{
// TODO there should/could be an api to retrieve mails related to a topic somewhere else than in timeline
// generator ...
logger.debug("Retrieving emails linked to topic with id " + topicid);
final TreeMap<Long, TopicEventBubble> bubblesInfo = new TreeMap<Long, TopicEventBubble>();
String xwql_topic =
"select doc.fullName, mail.date, mail.messagesubject ,mail.from from Document doc, "
+ "doc.object(" + XWikiPersistence.CLASS_MAILS + ") as mail where mail.topicid='" + topicid
+ "' and doc.space='MailArchiveItems' order by mail.date asc";
final List<Object[]> msgs = queryManager.createQuery(xwql_topic, Query.XWQL).execute();
String previousSubject = StringUtils.normalizeSpace(topicsubject);
for (Object[] msg : msgs) {
final String docfullname = (String) msg[0];
final Date maildate = (Date) msg[1];
String mailmessagesubject = (String) msg[2];
final String mailfrom = (String) msg[3];
IMAUser parsedUser = mailUtils.parseUser(mailfrom, config.isMatchLdap());
String user = parsedUser.getDisplayName();
String link = this.userStatsUrl + "?user=" + mailfrom;
if (StringUtils.isNotEmpty(parsedUser.getWikiProfile())) {
link += "&wikiUser=" + parsedUser.getWikiProfile();
}
mailmessagesubject = StringUtils.normalizeSpace(mailmessagesubject);
String subject = mailmessagesubject.replace(previousSubject, "...");
previousSubject = mailmessagesubject;
TopicEventBubble bubbleEvent = new TopicEventBubble();
bubbleEvent.date = maildate;
bubbleEvent.url = xwiki.getDocument(docResolver.resolve(docfullname), context).getURL("view", context);
bubbleEvent.subject = subject;
bubbleEvent.link = link;
bubbleEvent.user = user;
bubblesInfo.put(maildate.getTime(), bubbleEvent);
}
return bubblesInfo;
}
private String getAuthorAvatar(final String user)
{
String authorAvatar = null;
String imgName = null;
try {
XWikiDocument userDoc = xwiki.getDocument(user, context);
if (userDoc != null && !userDoc.isNew()) {
BaseObject userObj = userDoc.getObject("XWiki.XWikiUsers");
if (userObj != null) {
imgName = userObj.getStringValue("avatar");
}
}
if (imgName == null) {
authorAvatar =
xwiki.getDocument("XWiki.XWikiUserSheet", context).getURL("download", context) + "/noavatar.png";
} else {
authorAvatar = userDoc.getURL("download", context) + '/' + imgName;
}
} catch (XWikiException e) {
logger.error("Failed to retrieve author avatar", e);
}
return authorAvatar;
}
/**
* Formats a timeline bubble content, ie html presenting list of mails related to a given topic.
*
* @param topicid
* @param topicsubject
* @return
* @throws QueryException
* @throws XWikiException
*/
protected String getExtract(String topicid) throws QueryException, XWikiException
{
String extract = "";
logger.debug("Retrieving first email linked to topic with id " + topicid);
String xwql_topic =
"select mail.body, mail.bodyhtml from Document doc, " + "doc.object(" + XWikiPersistence.CLASS_MAILS
+ ") as mail where mail.topicid='" + topicid
+ "' and doc.space='MailArchiveItems' order by mail.date asc";
final List<Object[]> msgs = queryManager.createQuery(xwql_topic, Query.XWQL).setLimit(1).execute();
if (CollectionUtils.isNotEmpty(msgs)) {
final String body = (String) msgs.get(0)[0];
final String bodyhtml = (String) msgs.get(0)[1];
extract = body;
try {
DecodedMailContent decoded = mailUtils.decodeMailContent(bodyhtml, body, true);
if (decoded != null) {
if (decoded.isHtml() && StringUtils.isEmpty(decoded.getText())) {
extract = textUtils.htmlToPlainText(bodyhtml);
} else {
extract = decoded.getText();
}
}
} catch (IOException e) {
Log.warn("Could not decoded HTML content {}", e);
}
extract = StringUtils.normalizeSpace(extract);
extract = StringUtils.abbreviate(extract, 200);
}
return extract;
}
}