org.apache.james.mailbox.cassandra.mail.CassandraMailboxMapper.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.mailbox.cassandra.mail.CassandraMailboxMapper.java

Source

/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you 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 org.apache.james.mailbox.cassandra.mail;

import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
import org.apache.james.mailbox.cassandra.CassandraId;
import org.apache.james.mailbox.exception.MailboxException;
import org.apache.james.mailbox.exception.MailboxExistsException;
import org.apache.james.mailbox.exception.MailboxNotFoundException;
import org.apache.james.mailbox.exception.TooLongMailboxNameException;
import org.apache.james.mailbox.model.MailboxACL;
import org.apache.james.mailbox.model.MailboxId;
import org.apache.james.mailbox.model.MailboxPath;
import org.apache.james.mailbox.store.mail.MailboxMapper;
import org.apache.james.mailbox.store.mail.model.Mailbox;
import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox;
import org.apache.james.util.CompletableFutureUtil;
import org.apache.james.util.OptionalConverter;

import com.datastax.driver.core.Session;
import com.datastax.driver.core.exceptions.InvalidQueryException;
import com.github.steveash.guavate.Guavate;
import com.google.common.base.Preconditions;

public class CassandraMailboxMapper implements MailboxMapper {

    public static final String WILDCARD = "%";
    public static final String VALUES_MAY_NOT_BE_LARGER_THAN_64_K = "Index expression values may not be larger than 64K";
    public static final String CLUSTERING_COLUMNS_IS_TOO_LONG = "The sum of all clustering columns is too long";

    private final int maxRetry;
    private final CassandraAsyncExecutor cassandraAsyncExecutor;
    private final CassandraMailboxPathDAO mailboxPathDAO;
    private final CassandraMailboxDAO mailboxDAO;
    private final Session session;

    public CassandraMailboxMapper(Session session, CassandraMailboxDAO mailboxDAO,
            CassandraMailboxPathDAO mailboxPathDAO, int maxRetry) {
        this.maxRetry = maxRetry;
        this.cassandraAsyncExecutor = new CassandraAsyncExecutor(session);
        this.mailboxDAO = mailboxDAO;
        this.mailboxPathDAO = mailboxPathDAO;
        this.session = session;
    }

    @Override
    public void delete(Mailbox mailbox) throws MailboxException {
        CassandraId mailboxId = (CassandraId) mailbox.getMailboxId();
        mailboxPathDAO.delete(mailbox.generateAssociatedPath()).thenCompose(any -> mailboxDAO.delete(mailboxId))
                .join();
    }

    @Override
    public Mailbox findMailboxByPath(MailboxPath path) throws MailboxException {
        try {
            return mailboxPathDAO.retrieveId(path).thenCompose(cassandraIdOptional -> cassandraIdOptional
                    .map(CassandraMailboxPathDAO.CassandraIdAndPath::getCassandraId)
                    .map(mailboxDAO::retrieveMailbox).orElse(CompletableFuture.completedFuture(Optional.empty())))
                    .join().orElseThrow(() -> new MailboxNotFoundException(path));
        } catch (CompletionException e) {
            if (e.getCause() instanceof InvalidQueryException) {
                if (StringUtils.containsIgnoreCase(e.getCause().getMessage(), VALUES_MAY_NOT_BE_LARGER_THAN_64_K)) {
                    throw new TooLongMailboxNameException("too long mailbox name");
                }
                throw new MailboxException("It has error with cassandra storage", e.getCause());
            }
            throw e;
        }
    }

    @Override
    public Mailbox findMailboxById(MailboxId id) throws MailboxException {
        CassandraId mailboxId = (CassandraId) id;
        return mailboxDAO.retrieveMailbox(mailboxId).join()
                .orElseThrow(() -> new MailboxNotFoundException(id.serialize()));
    }

    @Override
    public List<Mailbox> findMailboxWithPathLike(MailboxPath path) throws MailboxException {
        Pattern regex = Pattern.compile(constructEscapedRegexForMailboxNameMatching(path));
        return mailboxPathDAO.listUserMailboxes(path.getNamespace(), path.getUser())
                .thenApply(stream -> stream
                        .filter(idAndPath -> regex.matcher(idAndPath.getMailboxPath().getName()).matches()))
                .thenApply(stream -> stream.map(CassandraMailboxPathDAO.CassandraIdAndPath::getCassandraId))
                .thenApply(stream -> stream.map(mailboxDAO::retrieveMailbox))
                .thenCompose(CompletableFutureUtil::allOf).thenApply(stream -> stream
                        .flatMap(OptionalConverter::toStream).collect(Guavate.<Mailbox>toImmutableList()))
                .join();
    }

    @Override
    public MailboxId save(Mailbox mailbox) throws MailboxException {
        Preconditions.checkArgument(mailbox instanceof SimpleMailbox);
        SimpleMailbox cassandraMailbox = (SimpleMailbox) mailbox;

        CassandraId cassandraId = retrieveId(cassandraMailbox);
        cassandraMailbox.setMailboxId(cassandraId);
        try {
            boolean applied = trySave(cassandraMailbox, cassandraId).join();
            if (!applied) {
                throw new MailboxExistsException(mailbox.generateAssociatedPath().asString());
            }
        } catch (CompletionException e) {
            manageException(e);
        }
        return cassandraId;
    }

    private CompletableFuture<Boolean> trySave(SimpleMailbox cassandraMailbox, CassandraId cassandraId) {
        return mailboxPathDAO.save(cassandraMailbox.generateAssociatedPath(), cassandraId).thenCompose(result -> {
            if (result) {
                return mailboxDAO
                        .retrieveMailbox(
                                cassandraId)
                        .thenCompose(optional -> CompletableFuture.allOf(optional
                                .map(storedMailbox -> mailboxPathDAO.delete(storedMailbox.generateAssociatedPath()))
                                .orElse(CompletableFuture.completedFuture(null)), mailboxDAO.save(cassandraMailbox))
                                .thenApply(any -> result));
            }
            return CompletableFuture.completedFuture(result);
        });
    }

    private void manageException(CompletionException e) throws MailboxException {
        if (e.getCause() instanceof InvalidQueryException) {
            String errorMessage = e.getCause().getMessage();
            if (StringUtils.containsIgnoreCase(errorMessage, VALUES_MAY_NOT_BE_LARGER_THAN_64_K)
                    || StringUtils.containsIgnoreCase(errorMessage, CLUSTERING_COLUMNS_IS_TOO_LONG)) {
                throw new TooLongMailboxNameException("too long mailbox name");
            }
            throw new MailboxException("It has error with cassandra storage", e.getCause());
        }
        throw e;
    }

    private CassandraId retrieveId(SimpleMailbox cassandraMailbox) {
        if (cassandraMailbox.getMailboxId() == null) {
            return CassandraId.timeBased();
        } else {
            return (CassandraId) cassandraMailbox.getMailboxId();
        }
    }

    @Override
    public boolean hasChildren(Mailbox mailbox, char delimiter) {
        return mailboxPathDAO.listUserMailboxes(mailbox.getNamespace(), mailbox.getUser())
                .thenApply(stream -> stream.anyMatch(idAndPath -> idAndPath.getMailboxPath().getName()
                        .startsWith(mailbox.getName() + String.valueOf(delimiter))))
                .join();
    }

    @Override
    public List<Mailbox> list() throws MailboxException {
        return mailboxDAO.retrieveAllMailboxes().join().collect(Guavate.toImmutableList());
    }

    @Override
    public <T> T execute(Transaction<T> transaction) throws MailboxException {
        return transaction.run();
    }

    @Override
    public void updateACL(Mailbox mailbox, MailboxACL.MailboxACLCommand mailboxACLCommand) throws MailboxException {
        CassandraId cassandraId = (CassandraId) mailbox.getMailboxId();
        new CassandraACLMapper(cassandraId, session, cassandraAsyncExecutor, maxRetry).updateACL(mailboxACLCommand);
    }

    @Override
    public void endRequest() {
        // Do nothing
    }

    private String constructEscapedRegexForMailboxNameMatching(MailboxPath path) {
        return Collections.list(new StringTokenizer(path.getName(), WILDCARD, true)).stream()
                .map(this::tokenToPatternPart).collect(Collectors.joining());
    }

    private String tokenToPatternPart(Object token) {
        if (token.equals(WILDCARD)) {
            return ".*";
        } else {
            return Pattern.quote((String) token);
        }
    }

}