/** * 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); } } }