/**
* MailArchiver is an application that provides services for storing and managing e-mail messages through a Web Services SOAP interface.
* Copyright (C) 2012 Marcio Andre Scholl Levien and Fernando Alberto Reuter Wendt and Jose Ronaldo Nogueira Fonseca Junior
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/******************************************************************************\
*
* This product was developed by
*
* SERVIÇO FEDERAL DE PROCESSAMENTO DE DADOS (SERPRO),
*
* a government company established under Brazilian law (5.615/70),
* at Department of Development of Porto Alegre.
*
\******************************************************************************/
package serpro.mailarchiver.service.web;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.Query;
import javax.jdo.annotations.PersistenceAware;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import com.ctc.wstx.stax.WstxInputFactory;
import org.codehaus.jettison.mapped.Configuration;
import org.codehaus.jettison.mapped.MappedXMLInputFactory;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMEvent;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.datanucleus.query.typesafe.BooleanExpression;
import org.datanucleus.query.typesafe.DateTimeExpression;
import org.datanucleus.query.typesafe.NumericExpression;
import org.datanucleus.query.typesafe.OrderExpression;
import org.datanucleus.query.typesafe.StringExpression;
import org.datanucleus.query.typesafe.TypesafeQuery;
import org.springframework.beans.factory.annotation.Autowired;
import serpro.mailarchiver.domain.metaarchive.Folder;
import serpro.mailarchiver.domain.metaarchive.Message;
import serpro.mailarchiver.domain.metaarchive.QDateTimeField;
import serpro.mailarchiver.domain.metaarchive.QFolder;
import serpro.mailarchiver.domain.metaarchive.QMailboxListField;
import serpro.mailarchiver.domain.metaarchive.QMailboxListField_Mailbox;
import serpro.mailarchiver.domain.metaarchive.QMessage;
import serpro.mailarchiver.domain.metaarchive.QUnstructuredField;
import serpro.mailarchiver.service.BaseService;
import serpro.mailarchiver.service.dto.TMessage;
import serpro.mailarchiver.service.find.FFolder;
import serpro.mailarchiver.session.Session;
import serpro.mailarchiver.util.Logger;
import serpro.mailarchiver.util.jdo.PersistenceManager;
import serpro.mailarchiver.util.transaction.WithReadOnlyTx;
@PersistenceAware
public class DefaultListMessagesOperation
extends BaseService
implements ListMessagesOperation
{
private enum Order {
FromAsc, FromDesc,
SubjectAsc, SubjectDesc,
DateAsc, DateDesc,
SizeAsc, SizeDesc
}
private static final Logger log = Logger.getLocalLogger();
private static final boolean _DEBUG_ = true;
@Autowired
private FFolder findFolder;
@WithReadOnlyTx
@Override
public TMessage[] apply(String queryConfig) throws ServiceFault {
/*
* queryConfig:
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
PersistenceManager pm = getPersistenceManager();
if(queryConfig.isEmpty()) {
ServiceFault.invalidQueryConfig()
.setActor("listMessages")
.setMessage("Query config is null or empty.")
.raise();
}
XMLInputFactory inf = null;
switch(queryConfig.charAt(0)) {
case '<':
inf = new WstxInputFactory();
break;
case '{':
Configuration config = new Configuration();
config.setIgnoreNamespaces(true);
inf = new MappedXMLInputFactory(config);
break;
default:
ServiceFault.invalidQueryConfig()
.setActor("listMessages")
.setMessage("Invalid query config.")
.raise();
}
String bodyCriteria = null;
String subjectCriteria = null;
String fromCriteria = null;
String toCriteria = null;
String toOrCcCriteria = null;
String toOrCcOrBccCriteria = null;
String ccCriteria = null;
String ccOrBccCriteria = null;
String bccCriteria = null;
String queryExpression = null;
TypesafeQuery tq = pm.newTypesafeQuery(Message.class);
QMessage cand = QMessage.candidate();
class NameGenerator {
private int variableCount = 0;
String var() {
return "v" + (++variableCount);
}
private int parameterCount = 0;
String par() {
return "p" + (++parameterCount);
}
}
NameGenerator next = new NameGenerator();
BooleanExpression folderCriteria = null;
BooleanExpression dateCriteria = null;
BooleanExpression tagCriteria = null;
BooleanExpression criteria = null;
Set targetFolders = new HashSet();
List ordering = new ArrayList();
long rangeStart = -1;
long rangeEnd = -1;
Map params = new TreeMap();
SMInputFactory sminf = new SMInputFactory(inf);
StringReader strReader = new StringReader(queryConfig);
try {
SMHierarchicCursor rootCursor = sminf.rootElementCursor(strReader);
rootCursor.advance();
String rootLocalName = rootCursor.getLocalName();
//------------------------------------------------------------------
if(rootLocalName.equalsIgnoreCase("query")) {
for(int i = 0; i < rootCursor.getAttrCount(); i++) {
String attrLocalName = rootCursor.getAttrLocalName(i);
if(attrLocalName.equalsIgnoreCase("body")) {
String arg = rootCursor.getAttrValue(i);
bodyCriteria = String.format("(body:(%s))", arg);
}
else if(attrLocalName.equalsIgnoreCase("subject")) {
String arg = rootCursor.getAttrValue(i);
subjectCriteria = String.format("(subject:(%s))", arg);
}
else if(attrLocalName.equalsIgnoreCase("from")) {
String arg = rootCursor.getAttrValue(i);
fromCriteria = String.format("(from:(%s) OR from_mbox:(% p = tq.datetimeParameter(pName);
String vName = next.var();
QDateTimeField v = QDateTimeField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("date"))
.and(v.date.gteq(p));
tq.setParameter(pName, new Date(Long.parseLong(arg)));
params.put(pName, arg);
dateCriteria = (dateCriteria == null) ? (expr) : dateCriteria.and(expr);
}
else if(attrLocalName.equalsIgnoreCase("upperDate")) {
String arg = rootCursor.getAttrValue(i);
String pName = next.par();
DateTimeExpression p = tq.datetimeParameter(pName);
String vName = next.var();
QDateTimeField v = QDateTimeField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("date"))
.and(v.date.lteq(p));
tq.setParameter(pName, new Date(Long.parseLong(arg)));
params.put(pName, arg);
dateCriteria = (dateCriteria == null) ? (expr) : dateCriteria.and(expr);
}
else if(attrLocalName.equalsIgnoreCase("date")) {
String arg = rootCursor.getAttrValue(i);
String pName = next.par();
DateTimeExpression p = tq.datetimeParameter(pName);
String qName = next.par();
DateTimeExpression q = tq.datetimeParameter(qName);
String vName = next.var();
QDateTimeField v = QDateTimeField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("date"))
.and(v.date.gteq(p))
.and(v.date.lteq(q));
Calendar calendar = new GregorianCalendar();
calendar.setTimeInMillis(Long.parseLong(arg));
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
long lower = calendar.getTimeInMillis();
tq.setParameter(pName, new Date(lower));
params.put(pName, String.valueOf(lower));
calendar.add(Calendar.DAY_OF_MONTH, 1);
long upper = calendar.getTimeInMillis();
tq.setParameter(qName, new Date(upper));
params.put(qName, String.valueOf(upper));
dateCriteria = (dateCriteria == null) ? (expr) : dateCriteria.and(expr);
}
else if(attrLocalName.equalsIgnoreCase("lowerIndex")) {
String arg = rootCursor.getAttrValue(i);
rangeStart = Long.parseLong(arg);
}
else if(attrLocalName.equalsIgnoreCase("upperIndex")) {
String arg = rootCursor.getAttrValue(i);
rangeEnd = Long.parseLong(arg);
}
else {
//ignore ?
}
}
SMInputCursor childCursor = rootCursor.childElementCursor();
SMEvent nextChild;
while((nextChild = childCursor.getNext()) != null) {
String childLocalName = childCursor.getLocalName();
//----------------------------------------------------------
if(childLocalName.equalsIgnoreCase("folder")) {
String folderId = "";
boolean recursive = false;
for(int i = 0; i < childCursor.getAttrCount(); i++) {
String attrLocalName = childCursor.getAttrLocalName(i);
if(attrLocalName.equalsIgnoreCase("id")) {
folderId = childCursor.getAttrValue(i);
}
else if(attrLocalName.equalsIgnoreCase("recursive")) {
recursive = childCursor.getAttrBooleanValue(i);
}
else {
//ignore ?
}
}
if(folderId.isEmpty()) {
ServiceFault.invalidFolderId()
.setActor("listMessages")
.setMessage("Folder id is empty.")
.raise();
}
Folder folder = findFolder.byId(folderId);
if(folder == null) {
ServiceFault.folderNotFound()
.setActor("listMessages")
.setMessage("Folder not found.")
.addValue("folderId", folderId)
.raise();
}
if(recursive) {
targetFolders.addAll(listFolders(folder));
}
else {
targetFolders.add(folder);
}
}
//----------------------------------------------------------
else if(childLocalName.equalsIgnoreCase("tags")) {
for(int i = 0; i < childCursor.getAttrCount(); i++) {
String attrLocalName = childCursor.getAttrLocalName(i);
if(attrLocalName.equalsIgnoreCase("contains")) {
String arg = childCursor.getAttrValue(i);
String pName = next.par();
StringExpression p = tq.stringParameter(pName);
BooleanExpression expr = cand.tags.contains(p);
tq.setParameter(pName, arg.toLowerCase());
params.put(pName, arg);
tagCriteria = (tagCriteria == null) ? (expr) : tagCriteria.and(expr);
}
else if(attrLocalName.equalsIgnoreCase("notContains")) {
String arg = childCursor.getAttrValue(i);
String pName = next.par();
StringExpression p = tq.stringParameter(pName);
BooleanExpression expr = cand.tags.contains(p).not();
tq.setParameter(pName, arg.toLowerCase());
params.put(pName, arg);
tagCriteria = (tagCriteria == null) ? (expr) : tagCriteria.and(expr);
}
else {
//ignore ?
}
}
}
//----------------------------------------------------------
else if(childLocalName.equalsIgnoreCase("order")) {
for(int i = 0; i < childCursor.getAttrCount(); i++) {
String attrLocalName = childCursor.getAttrLocalName(i);
if(attrLocalName.equalsIgnoreCase("from")) {
String arg = childCursor.getAttrValue(i);
if(arg.equalsIgnoreCase("asc")) {
ordering.add(Order.FromAsc);
}
else if(arg.equalsIgnoreCase("desc")) {
ordering.add(Order.FromDesc);
}
else {
//ignore ?
}
}
else if(attrLocalName.equalsIgnoreCase("subject")) {
String arg = childCursor.getAttrValue(i);
if(arg.equalsIgnoreCase("asc")) {
ordering.add(Order.SubjectAsc);
}
else if(arg.equalsIgnoreCase("desc")) {
ordering.add(Order.SubjectDesc);
}
else {
//ignore ?
}
}
else if(attrLocalName.equalsIgnoreCase("date")) {
String arg = childCursor.getAttrValue(i);
if(arg.equalsIgnoreCase("asc")) {
ordering.add(Order.DateAsc);
}
else if(arg.equalsIgnoreCase("desc")) {
ordering.add(Order.DateDesc);
}
else {
//ignore ?
}
}
else if(attrLocalName.equalsIgnoreCase("size")) {
String arg = childCursor.getAttrValue(i);
if(arg.equalsIgnoreCase("asc")) {
ordering.add(Order.SizeAsc);
}
else if(arg.equalsIgnoreCase("desc")) {
ordering.add(Order.SizeDesc);
}
else {
//ignore ?
}
}
else {
//ignore ?
}
}
}
//----------------------------------------------------------
else {
//ignore ?
}
}
}
else {
//ignore ?
}
}
catch(XMLStreamException ex) {
ServiceFault.runtimeException()
.setActor("listMessages")
.setMessage("Query config parse exception.")
.setCause(ex)
.raise();
}
if(bodyCriteria != null) {
queryExpression = (queryExpression == null) ? bodyCriteria : queryExpression.concat(" AND ").concat(bodyCriteria);
}
if(subjectCriteria != null) {
queryExpression = (queryExpression == null) ? subjectCriteria : queryExpression.concat(" AND ").concat(subjectCriteria);
}
if(fromCriteria != null) {
queryExpression = (queryExpression == null) ? fromCriteria : queryExpression.concat(" AND ").concat(fromCriteria);
}
if(toCriteria != null) {
queryExpression = (queryExpression == null) ? toCriteria : queryExpression.concat(" AND ").concat(toCriteria);
}
if(toOrCcCriteria != null) {
queryExpression = (queryExpression == null) ? toOrCcCriteria : queryExpression.concat(" AND ").concat(toOrCcCriteria);
}
if(toOrCcOrBccCriteria != null) {
queryExpression = (queryExpression == null) ? toOrCcOrBccCriteria : queryExpression.concat(" AND ").concat(toOrCcOrBccCriteria);
}
if(ccCriteria != null) {
queryExpression = (queryExpression == null) ? ccCriteria : queryExpression.concat(" AND ").concat(ccCriteria);
}
if(ccOrBccCriteria != null) {
queryExpression = (queryExpression == null) ? ccOrBccCriteria : queryExpression.concat(" AND ").concat(ccOrBccCriteria);
}
if(bccCriteria != null) {
queryExpression = (queryExpression == null) ? bccCriteria : queryExpression.concat(" AND ").concat(bccCriteria);
}
Long queryCandidatesSet = null;
if(queryExpression != null) {
try {
for(String id : Session.getLuceneIndex().search(queryExpression)) {
if(queryCandidatesSet == null) {
Query q = pm.newQuery(javax.jdo.Query.SQL, "SELECT NEXTVAL('METAARCHIVE', 'QUERY_CANDIDATES_SET')");
q.setUnique(true);
q.setResultClass(Long.class);
queryCandidatesSet = (Long) q.execute();
}
try {
Message candidate = pm.getObjectById(Message.class, id);
candidate.setQueryCandidatesSet(queryCandidatesSet);
}
catch(JDOObjectNotFoundException e) {
log.warn(e, "message not found: %s", id);
}
}
}
catch(IOException ex) {
ServiceFault.fileSystemFailure()
.setActor("listMessages")
.setMessage("Lucene search failure.")
.setCause(ex)
.raise();
}
if(queryCandidatesSet == null) {
return new TMessage[]{};
}
}
if(queryCandidatesSet != null) {
String pName = next.par();
NumericExpression p = tq.longParameter(pName);
criteria = cand.queryCandidatesSet.eq(p);
tq.setParameter(pName, queryCandidatesSet);
params.put(pName, queryCandidatesSet.toString());
}
if(targetFolders.isEmpty()) {
Folder homeFolder = findFolder.byId("home");
if(homeFolder == null) {
ServiceFault.folderNotFound()
.setActor("listMessages")
.setMessage("Home folder not found.")
.raise();
}
targetFolders.addAll(listFolders(homeFolder));
}
{
String vName = next.var();
QFolder v = QFolder.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = null;
for(Folder folder : targetFolders) {
String pName = next.par();
StringExpression p = tq.stringParameter(pName);
expr = (expr == null) ? v.oid.eq(p) : expr.or(v.oid.eq(p));
tq.setParameter(pName, folder.getOid());
params.put(pName, folder.getOid());
}
folderCriteria = cand.folder.eq(v).and(expr);
criteria = (criteria == null) ? (folderCriteria) : criteria.and(folderCriteria);
}
if(dateCriteria != null) {
criteria = criteria.and(dateCriteria);
}
if(tagCriteria != null) {
criteria = criteria.and(tagCriteria);
}
List orderExpressions = new ArrayList();
for(Order order : ordering) {
switch(order) {
case FromAsc:
{
String vName = next.var();
QMailboxListField v = QMailboxListField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
String wName = next.var();
QMailboxListField_Mailbox w = QMailboxListField_Mailbox.variable(wName);
tq.addExtension("datanucleus.query.jdoql." + wName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("from"))
.and(v.mailboxList.contains(w));
criteria = criteria.and(expr);
//orderExpressions.add(w.name.asc());
orderExpressions.add(w.localPart.toUpperCase().asc());
orderExpressions.add(w.domain.toUpperCase().asc());
}
break;
case FromDesc:
{
String vName = next.var();
QMailboxListField v = QMailboxListField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
String wName = next.var();
QMailboxListField_Mailbox w = QMailboxListField_Mailbox.variable(wName);
tq.addExtension("datanucleus.query.jdoql." + wName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("from"))
.and(v.mailboxList.contains(w));
criteria = criteria.and(expr);
//orderExpressions.add(w.name.desc());
orderExpressions.add(w.localPart.toUpperCase().desc());
orderExpressions.add(w.domain.toUpperCase().desc());
}
break;
case SubjectAsc:
{
String vName = next.var();
QUnstructuredField v = QUnstructuredField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("subject"));
criteria = criteria.and(expr);
orderExpressions.add(v.text.toUpperCase().asc());
}
break;
case SubjectDesc:
{
String vName = next.var();
QUnstructuredField v = QUnstructuredField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("subject"));
criteria = criteria.and(expr);
orderExpressions.add(v.text.toUpperCase().desc());
}
break;
case DateAsc:
{
String vName = next.var();
QDateTimeField v = QDateTimeField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("date"));
criteria = criteria.and(expr);
orderExpressions.add(v.date.asc());
}
break;
case DateDesc:
{
String vName = next.var();
QDateTimeField v = QDateTimeField.variable(vName);
tq.addExtension("datanucleus.query.jdoql." + vName + ".join", "INNERJOIN");
BooleanExpression expr = cand.fields.contains(v)
.and(v.name.equalsIgnoreCase("date"));
criteria = criteria.and(expr);
orderExpressions.add(v.date.desc());
}
break;
case SizeAsc:
{
orderExpressions.add(cand.size.asc());
}
break;
case SizeDesc:
{
orderExpressions.add(cand.size.desc());
}
break;
default:
{
}
}
}
tq.filter(criteria);
if(orderExpressions.size() > 0) {
tq.orderBy(orderExpressions.toArray(new OrderExpression[orderExpressions.size()]));
}
if((rangeStart >= 0) && (rangeEnd >= 0) && (rangeStart <= rangeEnd)) {
tq.range(rangeStart, rangeEnd);
}
tq.addExtension("datanucleus.sqlTableNamingStrategy", "t-scheme");
if(_DEBUG_) {
try {
System.out.println("Lucene Query Expression:\n" + queryExpression);
String ssq = tq.toString();
System.out.println("JDOQL Single-String Query:\n" + ssq);
StringBuilder sb = new StringBuilder();
sb.append("JDOQL Query Parameters:\n");
for(Entry param : params.entrySet()) {
sb.append("\t").append(param.getKey()).append("=").append(param.getValue()).append("\n");
}
System.out.println(sb.toString());
}
catch(UnsupportedOperationException ex) {
//Dont currently support operator NOT in JDOQL conversion
}
}
List results = tq.executeList();
int size = results.size();
TMessage[] messageDtoArray = new TMessage[size];
for(int i = 0; i < size; i++) {
messageDtoArray[i] = new TMessage(results.get(i));
}
return messageDtoArray;
}
private List listFolders(Folder folder) {
List list = new ArrayList();
listFolders(folder, list);
return list;
}
private void listFolders(Folder folder, List list) {
list.add(folder);
for(Folder child : folder.getChildren()) {
listFolders(child, list);
}
}
}