001/****************************************************************
002 * Licensed to the Apache Software Foundation (ASF) under one   *
003 * or more contributor license agreements.  See the NOTICE file *
004 * distributed with this work for additional information        *
005 * regarding copyright ownership.  The ASF licenses this file   *
006 * to you under the Apache License, Version 2.0 (the            *
007 * "License"); you may not use this file except in compliance   *
008 * with the License.  You may obtain a copy of the License at   *
009 *                                                              *
010 *   http://www.apache.org/licenses/LICENSE-2.0                 *
011 *                                                              *
012 * Unless required by applicable law or agreed to in writing,   *
013 * software distributed under the License is distributed on an  *
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
015 * KIND, either express or implied.  See the License for the    *
016 * specific language governing permissions and limitations      *
017 * under the License.                                           *
018 ****************************************************************/
019package org.apache.james.mailbox.jpa.mail;
020
021import java.util.HashMap;
022import java.util.Iterator;
023import java.util.List;
024import java.util.Map;
025
026import javax.persistence.EntityManager;
027import javax.persistence.EntityManagerFactory;
028import javax.persistence.EntityTransaction;
029import javax.persistence.PersistenceException;
030import javax.persistence.Query;
031
032import org.apache.james.mailbox.MailboxSession;
033import org.apache.james.mailbox.exception.MailboxException;
034import org.apache.james.mailbox.jpa.mail.model.JPAMailbox;
035import org.apache.james.mailbox.jpa.mail.model.openjpa.AbstractJPAMessage;
036import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAEncryptedMessage;
037import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAMessage;
038import org.apache.james.mailbox.jpa.mail.model.openjpa.JPAStreamingMessage;
039import org.apache.james.mailbox.model.MessageMetaData;
040import org.apache.james.mailbox.model.MessageRange;
041import org.apache.james.mailbox.model.MessageRange.Type;
042import org.apache.james.mailbox.store.SimpleMessageMetaData;
043import org.apache.james.mailbox.store.mail.AbstractMessageMapper;
044import org.apache.james.mailbox.store.mail.MessageMapper;
045import org.apache.james.mailbox.store.mail.ModSeqProvider;
046import org.apache.james.mailbox.store.mail.UidProvider;
047import org.apache.james.mailbox.store.mail.model.Mailbox;
048import org.apache.james.mailbox.store.mail.model.Message;
049import org.apache.openjpa.persistence.ArgumentException;
050
051/**
052 * JPA implementation of a {@link MessageMapper}. This class is not thread-safe!
053 */
054public class JPAMessageMapper extends AbstractMessageMapper<Long> implements MessageMapper<Long> {
055    protected EntityManagerFactory entityManagerFactory;
056    protected EntityManager entityManager;
057
058    public JPAMessageMapper(final MailboxSession session, final UidProvider<Long> uidProvider,
059            ModSeqProvider<Long> modSeqProvider, final EntityManagerFactory entityManagerFactory) {
060        super(session, uidProvider, modSeqProvider);
061        this.entityManagerFactory = entityManagerFactory;
062    }
063
064    /**
065     * Return the currently used {@link EntityManager} or a new one if none
066     * exists.
067     * 
068     * @return entitymanger
069     */
070    public EntityManager getEntityManager() {
071        if (entityManager != null)
072            return entityManager;
073        entityManager = entityManagerFactory.createEntityManager();
074        return entityManager;
075    }
076
077    /**
078     * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#begin()
079     */
080    protected void begin() throws MailboxException {
081        try {
082            getEntityManager().getTransaction().begin();
083        } catch (PersistenceException e) {
084            throw new MailboxException("Begin of transaction failed", e);
085        }
086    }
087
088    /**
089     * Commit the Transaction and close the EntityManager
090     */
091    protected void commit() throws MailboxException {
092        try {
093            getEntityManager().getTransaction().commit();
094        } catch (PersistenceException e) {
095            throw new MailboxException("Commit of transaction failed", e);
096        }
097    }
098
099    /**
100     * @see org.apache.james.mailbox.store.transaction.TransactionalMapper#rollback()
101     */
102    protected void rollback() throws MailboxException {
103        EntityTransaction transaction = entityManager.getTransaction();
104        // check if we have a transaction to rollback
105        if (transaction.isActive()) {
106            getEntityManager().getTransaction().rollback();
107        }
108    }
109
110    /**
111     * Close open {@link EntityManager}
112     */
113    public void endRequest() {
114        if (entityManager != null) {
115            if (entityManager.isOpen())
116                entityManager.close();
117            entityManager = null;
118        }
119    }
120
121    /**
122     * @see org.apache.james.mailbox.store.mail.MessageMapper#findInMailbox(org.apache.james.mailbox.store.mail.model.Mailbox,
123     *      org.apache.james.mailbox.model.MessageRange,
124     *      org.apache.james.mailbox.store.mail.MessageMapper.FetchType, int)
125     */
126    public Iterator<Message<Long>> findInMailbox(Mailbox<Long> mailbox, MessageRange set, FetchType fType, int max)
127            throws MailboxException {
128        try {
129            List<Message<Long>> results;
130            long from = set.getUidFrom();
131            final long to = set.getUidTo();
132            final Type type = set.getType();
133
134            switch (type) {
135            default:
136            case ALL:
137                results = findMessagesInMailbox(mailbox, max);
138                break;
139            case FROM:
140                results = findMessagesInMailboxAfterUID(mailbox, from, max);
141                break;
142            case ONE:
143                results = findMessagesInMailboxWithUID(mailbox, from);
144                break;
145            case RANGE:
146                results = findMessagesInMailboxBetweenUIDs(mailbox, from, to, max);
147                break;
148            }
149
150            return results.iterator();
151
152        } catch (PersistenceException e) {
153            throw new MailboxException("Search of MessageRange " + set + " failed in mailbox " + mailbox, e);
154        }
155    }
156
157    /**
158     * @see org.apache.james.mailbox.store.mail.MessageMapper#countMessagesInMailbox(Mailbox)
159     */
160    public long countMessagesInMailbox(Mailbox<Long> mailbox) throws MailboxException {
161        try {
162            return (Long) getEntityManager().createNamedQuery("countMessagesInMailbox")
163                    .setParameter("idParam", mailbox.getMailboxId()).getSingleResult();
164        } catch (PersistenceException e) {
165            throw new MailboxException("Count of messages failed in mailbox " + mailbox, e);
166        }
167    }
168
169    /**
170     * @see org.apache.james.mailbox.store.mail.MessageMapper#countUnseenMessagesInMailbox(Mailbox)
171     */
172    public long countUnseenMessagesInMailbox(Mailbox<Long> mailbox) throws MailboxException {
173        try {
174            return (Long) getEntityManager().createNamedQuery("countUnseenMessagesInMailbox")
175                    .setParameter("idParam", mailbox.getMailboxId()).getSingleResult();
176        } catch (PersistenceException e) {
177            throw new MailboxException("Count of useen messages failed in mailbox " + mailbox, e);
178        }
179    }
180
181    /**
182     * @see org.apache.james.mailbox.store.mail.MessageMapper#delete(org.apache.james.mailbox.store.mail.model.Mailbox,
183     *      org.apache.james.mailbox.store.mail.model.Message)
184     */
185    public void delete(Mailbox<Long> mailbox, Message<Long> message) throws MailboxException {
186        try {
187            getEntityManager().remove(message);
188        } catch (PersistenceException e) {
189            throw new MailboxException("Delete of message " + message + " failed in mailbox " + mailbox, e);
190        }
191    }
192
193    /**
194     * @see org.apache.james.mailbox.store.mail.MessageMapper#findFirstUnseenMessageUid(Mailbox)
195     */
196    @SuppressWarnings("unchecked")
197    public Long findFirstUnseenMessageUid(Mailbox<Long> mailbox) throws MailboxException {
198        try {
199            Query query = getEntityManager().createNamedQuery("findUnseenMessagesInMailboxOrderByUid").setParameter(
200                    "idParam", mailbox.getMailboxId());
201            query.setMaxResults(1);
202            List<Message<Long>> result = query.getResultList();
203            if (result.isEmpty()) {
204                return null;
205            } else {
206                return result.get(0).getUid();
207            }
208        } catch (PersistenceException e) {
209            throw new MailboxException("Search of first unseen message failed in mailbox " + mailbox, e);
210        }
211    }
212
213    /**
214     * @see org.apache.james.mailbox.store.mail.MessageMapper#findRecentMessageUidsInMailbox(Mailbox)
215     */
216    @SuppressWarnings("unchecked")
217    public List<Long> findRecentMessageUidsInMailbox(Mailbox<Long> mailbox) throws MailboxException {
218        try {
219            Query query = getEntityManager().createNamedQuery("findRecentMessageUidsInMailbox").setParameter("idParam",
220                    mailbox.getMailboxId());
221            return query.getResultList();
222        } catch (PersistenceException e) {
223            throw new MailboxException("Search of recent messages failed in mailbox " + mailbox, e);
224        }
225    }
226
227    @Override
228    public Map<Long, MessageMetaData> expungeMarkedForDeletionInMailbox(Mailbox<Long> mailbox, MessageRange set)
229            throws MailboxException {
230        try {
231            final Map<Long, MessageMetaData> data;
232            final List<Message<Long>> results;
233            final long from = set.getUidFrom();
234            final long to = set.getUidTo();
235
236            switch (set.getType()) {
237            case ONE:
238                results = findDeletedMessagesInMailboxWithUID(mailbox, from);
239                data = createMetaData(results);
240                deleteDeletedMessagesInMailboxWithUID(mailbox, from);
241                break;
242            case RANGE:
243                results = findDeletedMessagesInMailboxBetweenUIDs(mailbox, from, to);
244                data = createMetaData(results);
245                deleteDeletedMessagesInMailboxBetweenUIDs(mailbox, from, to);
246                break;
247            case FROM:
248                results = findDeletedMessagesInMailboxAfterUID(mailbox, from);
249                data = createMetaData(results);
250                deleteDeletedMessagesInMailboxAfterUID(mailbox, from);
251                break;
252            default:
253            case ALL:
254                results = findDeletedMessagesInMailbox(mailbox);
255                data = createMetaData(results);
256                deleteDeletedMessagesInMailbox(mailbox);
257                break;
258            }
259
260            return data;
261        } catch (PersistenceException e) {
262            throw new MailboxException("Search of MessageRange " + set + " failed in mailbox " + mailbox, e);
263        }
264    }
265
266    /**
267     * (non-Javadoc)
268     * 
269     * @see org.apache.james.mailbox.store.mail.MessageMapper#move(org.apache.james.mailbox.store.mail.model.Mailbox,
270     *      org.apache.james.mailbox.store.mail.model.Message)
271     */
272    @Override
273    public MessageMetaData move(Mailbox<Long> mailbox, Message<Long> original) throws MailboxException {
274        throw new UnsupportedOperationException("Not implemented - see https://issues.apache.org/jira/browse/IMAP-370");
275    }
276
277    /**
278     * @see org.apache.james.mailbox.store.mail.AbstractMessageMapper#copy(Mailbox,
279     *      long, long, Message)
280     */
281    protected MessageMetaData copy(Mailbox<Long> mailbox, long uid, long modSeq, Message<Long> original)
282            throws MailboxException {
283        Message<Long> copy;
284        if (original instanceof JPAStreamingMessage) {
285            copy = new JPAStreamingMessage((JPAMailbox) mailbox, uid, modSeq, original);
286        } else if (original instanceof JPAEncryptedMessage) {
287            copy = new JPAEncryptedMessage((JPAMailbox) mailbox, uid, modSeq, original);
288        } else {
289            copy = new JPAMessage((JPAMailbox) mailbox, uid, modSeq, original);
290        }
291        return save(mailbox, copy);
292    }
293
294    /**
295     * @see org.apache.james.mailbox.store.mail.AbstractMessageMapper#save(Mailbox,
296     *      Message)
297     */
298    protected MessageMetaData save(Mailbox<Long> mailbox, Message<Long> message) throws MailboxException {
299
300        try {
301
302            // We need to reload a "JPA attached" mailbox, because the provide
303            // mailbox is already "JPA detached"
304            // If we don't this, we will get an
305            // org.apache.openjpa.persistence.ArgumentException.
306            ((AbstractJPAMessage) message)
307                    .setMailbox(getEntityManager().find(JPAMailbox.class, mailbox.getMailboxId()));
308
309            getEntityManager().persist(message);
310            return new SimpleMessageMetaData(message);
311        } catch (PersistenceException e) {
312            throw new MailboxException("Save of message " + message + " failed in mailbox " + mailbox, e);
313        } catch (ArgumentException e) {
314            throw new MailboxException("Save of message " + message + " failed in mailbox " + mailbox, e);
315        }
316    }
317
318    @SuppressWarnings("unchecked")
319    private List<Message<Long>> findMessagesInMailboxAfterUID(Mailbox<Long> mailbox, long uid, int batchSize) {
320        Query query = getEntityManager().createNamedQuery("findMessagesInMailboxAfterUID")
321                .setParameter("idParam", mailbox.getMailboxId()).setParameter("uidParam", uid);
322
323        if (batchSize > 0)
324            query.setMaxResults(batchSize);
325
326        return query.getResultList();
327    }
328
329    @SuppressWarnings("unchecked")
330    private List<Message<Long>> findMessagesInMailboxWithUID(Mailbox<Long> mailbox, long uid) {
331        return getEntityManager().createNamedQuery("findMessagesInMailboxWithUID")
332                .setParameter("idParam", mailbox.getMailboxId()).setParameter("uidParam", uid).setMaxResults(1)
333                .getResultList();
334    }
335
336    @SuppressWarnings("unchecked")
337    private List<Message<Long>> findMessagesInMailboxBetweenUIDs(Mailbox<Long> mailbox, long from, long to,
338            int batchSize) {
339        Query query = getEntityManager().createNamedQuery("findMessagesInMailboxBetweenUIDs")
340                .setParameter("idParam", mailbox.getMailboxId()).setParameter("fromParam", from)
341                .setParameter("toParam", to);
342
343        if (batchSize > 0)
344            query.setMaxResults(batchSize);
345
346        return query.getResultList();
347    }
348
349    @SuppressWarnings("unchecked")
350    private List<Message<Long>> findMessagesInMailbox(Mailbox<Long> mailbox, int batchSize) {
351        Query query = getEntityManager().createNamedQuery("findMessagesInMailbox").setParameter("idParam",
352                mailbox.getMailboxId());
353        if (batchSize > 0)
354            query.setMaxResults(batchSize);
355        return query.getResultList();
356    }
357
358    private Map<Long, MessageMetaData> createMetaData(List<Message<Long>> uids) {
359        final Map<Long, MessageMetaData> data = new HashMap<Long, MessageMetaData>();
360        for (int i = 0; i < uids.size(); i++) {
361            Message<Long> m = uids.get(i);
362            data.put(m.getUid(), new SimpleMessageMetaData(m));
363        }
364        return data;
365    }
366
367    private int deleteDeletedMessagesInMailbox(Mailbox<Long> mailbox) {
368        return getEntityManager().createNamedQuery("deleteDeletedMessagesInMailbox")
369                .setParameter("idParam", mailbox.getMailboxId()).executeUpdate();
370    }
371
372    private int deleteDeletedMessagesInMailboxAfterUID(Mailbox<Long> mailbox, long uid) {
373        return getEntityManager().createNamedQuery("deleteDeletedMessagesInMailboxAfterUID")
374                .setParameter("idParam", mailbox.getMailboxId()).setParameter("uidParam", uid).executeUpdate();
375    }
376
377    private int deleteDeletedMessagesInMailboxWithUID(Mailbox<Long> mailbox, long uid) {
378        return getEntityManager().createNamedQuery("deleteDeletedMessagesInMailboxWithUID")
379                .setParameter("idParam", mailbox.getMailboxId()).setParameter("uidParam", uid).executeUpdate();
380    }
381
382    private int deleteDeletedMessagesInMailboxBetweenUIDs(Mailbox<Long> mailbox, long from, long to) {
383        return getEntityManager().createNamedQuery("deleteDeletedMessagesInMailboxBetweenUIDs")
384                .setParameter("idParam", mailbox.getMailboxId()).setParameter("fromParam", from)
385                .setParameter("toParam", to).executeUpdate();
386    }
387
388    @SuppressWarnings("unchecked")
389    private List<Message<Long>> findDeletedMessagesInMailbox(Mailbox<Long> mailbox) {
390        return getEntityManager().createNamedQuery("findDeletedMessagesInMailbox")
391                .setParameter("idParam", mailbox.getMailboxId()).getResultList();
392    }
393
394    @SuppressWarnings("unchecked")
395    private List<Message<Long>> findDeletedMessagesInMailboxAfterUID(Mailbox<Long> mailbox, long uid) {
396        return getEntityManager().createNamedQuery("findDeletedMessagesInMailboxAfterUID")
397                .setParameter("idParam", mailbox.getMailboxId()).setParameter("uidParam", uid).getResultList();
398    }
399
400    @SuppressWarnings("unchecked")
401    private List<Message<Long>> findDeletedMessagesInMailboxWithUID(Mailbox<Long> mailbox, long uid) {
402        return getEntityManager().createNamedQuery("findDeletedMessagesInMailboxWithUID")
403                .setParameter("idParam", mailbox.getMailboxId()).setParameter("uidParam", uid).setMaxResults(1)
404                .getResultList();
405    }
406
407    @SuppressWarnings("unchecked")
408    private List<Message<Long>> findDeletedMessagesInMailboxBetweenUIDs(Mailbox<Long> mailbox, long from, long to) {
409        return getEntityManager().createNamedQuery("findDeletedMessagesInMailboxBetweenUIDs")
410                .setParameter("idParam", mailbox.getMailboxId()).setParameter("fromParam", from)
411                .setParameter("toParam", to).getResultList();
412    }
413}