se.inera.axel.shs.broker.messagestore.internal.MongoMessageLogService.java Source code

Java tutorial

Introduction

Here is the source code for se.inera.axel.shs.broker.messagestore.internal.MongoMessageLogService.java

Source

/**
 * Copyright (C) 2013 Inera AB (http://www.inera.se)
 *
 * This file is part of Inera Axel (http://code.google.com/p/inera-axel).
 *
 * Inera Axel 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 3 of the License, or
 * (at your option) any later version.
 *
 * Inera Axel 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 program.  If not, see <http://www.gnu.org/licenses/>
 */
package se.inera.axel.shs.broker.messagestore.internal;

import com.mongodb.MongoException;
import com.mongodb.WriteResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoDataIntegrityViolationException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import se.inera.axel.shs.broker.messagestore.*;
import se.inera.axel.shs.exception.OtherErrorException;
import se.inera.axel.shs.exception.ShsException;
import se.inera.axel.shs.mime.DataPart;
import se.inera.axel.shs.mime.ShsMessage;
import se.inera.axel.shs.processor.ShsManagementMarshaller;
import se.inera.axel.shs.xml.label.SequenceType;
import se.inera.axel.shs.xml.label.ShsLabel;
import se.inera.axel.shs.xml.label.TransferType;
import se.inera.axel.shs.xml.management.ShsManagement;

import javax.annotation.Resource;
import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * @author Jan Hallonstn, R2M
 *
 */
@Service("messageLogService")
public class MongoMessageLogService implements MessageLogService {
    Logger log = LoggerFactory.getLogger(MongoMessageLogService.class);

    @Resource
    private MessageLogRepository messageLogRepository;

    @Resource
    private MessageStoreService messageStoreService;

    @Resource
    MongoTemplate mongoTemplate;

    private final ShsManagementMarshaller marshaller = new ShsManagementMarshaller();

    @Override
    public ShsMessageEntry saveMessageStream(ShsLabel label, InputStream mimeMessageStream) {
        String id = UUID.randomUUID().toString();

        ShsMessageEntry shsMessageEntry = new ShsMessageEntry(id, label);

        log.debug("saveMessageStream(InputStream) saving file with id {}", id);
        shsMessageEntry = messageStoreService.save(shsMessageEntry, mimeMessageStream);

        if (shsMessageEntry.getLabel() == null)
            throw new OtherErrorException(String.format("Could not find message with id %s after save", id));

        try {
            shsMessageEntry = saveShsMessageEntry(shsMessageEntry);
        } catch (Exception e) {
            try {
                messageStoreService.delete(shsMessageEntry);
            } catch (Exception deleteException) {

            }
            throw e;
        }

        return shsMessageEntry;
    }

    @Override
    public ShsMessageEntry saveMessage(ShsMessage message) {
        String id = UUID.randomUUID().toString();
        ShsLabel label = message.getLabel();

        ShsMessageEntry shsMessageEntry = new ShsMessageEntry(id, label);
        ShsMessageEntry entry = saveShsMessageEntry(shsMessageEntry);

        messageStoreService.save(entry, message);

        return entry;
    }

    @Override
    public void deleteMessage(ShsMessageEntry messageEntry) {
        messageStoreService.delete(messageEntry);
        messageEntry.setArchived(true);
        update(messageEntry);
    }

    private ShsMessageEntry saveShsMessageEntry(ShsMessageEntry entry) {
        if (entry.getLabel() == null)
            throw new IllegalArgumentException("Label must not be null");

        entry.setState(MessageState.NEW);
        entry.setStateTimeStamp(new Date());
        entry.setArrivalTimeStamp(entry.getStateTimeStamp());

        ShsMessageEntry newEntry;
        ShsLabel label = entry.getLabel();

        try {
            newEntry = messageLogRepository.save(entry);
        } catch (MongoDataIntegrityViolationException e) {
            if (e.getWriteResult().getCachedLastError().getException() instanceof MongoException.DuplicateKey) {
                throw new MessageAlreadyExistsException(label, new Date(0));
            } else {
                throw e;
            }
        } catch (DuplicateKeyException e) {
            throw new MessageAlreadyExistsException(label, new Date(0));
        }

        return newEntry;
    }

    private void deleteShsMessageEntry(ShsMessageEntry entry) {

        messageLogRepository.delete(entry);

    }

    @Override
    public ShsMessageEntry messageReceived(ShsMessageEntry entry) {
        entry.setState(MessageState.RECEIVED);
        entry.setStateTimeStamp(new Date());
        return update(entry);
    }

    @Override
    public ShsMessageEntry messageSent(ShsMessageEntry entry) {
        entry.setState(MessageState.SENT);
        entry.setStateTimeStamp(new Date());
        return update(entry);
    }

    @Override
    public ShsMessageEntry messageFetched(ShsMessageEntry entry) {
        entry.setState(MessageState.FETCHED);
        entry.setStateTimeStamp(new Date());
        return update(entry);
    }

    @Override
    public ShsMessageEntry messageAcknowledged(ShsMessageEntry entry) {
        if (entry.isAcknowledged())
            return entry;

        entry.setAcknowledged(true);
        return update(entry);
    }

    @Override
    public ShsMessageEntry messageOneToMany(ShsMessageEntry entry) {
        entry.setState(MessageState.ONE_TO_MANY);
        entry.setStateTimeStamp(new Date());
        return update(entry);
    }

    @Override
    public ShsMessageEntry messageQuarantined(ShsMessageEntry entry, Exception exception) {

        if (exception instanceof ShsException) {
            ShsException shsException = (ShsException) exception;
            entry.setStatusCode(shsException.getErrorCode());
            if (shsException.getCause() != null) {
                entry.setStatusText(
                        shsException.getErrorInfo() + " (" + shsException.getCause().getMessage() + ")");
            } else {
                entry.setStatusText(shsException.getErrorInfo());
            }
        } else if (exception instanceof Exception) {
            entry.setStatusCode(exception.getClass().getSimpleName());
            if (exception.getCause() != null) {
                entry.setStatusText(exception.getMessage() + " (" + exception.getCause().getMessage() + ")");
            } else {
                entry.setStatusText(exception.getMessage());
            }
        }

        entry.setState(MessageState.QUARANTINED);
        entry.setStateTimeStamp(new Date());

        return update(entry);
    }

    @Override
    public ShsMessage quarantineCorrelatedMessages(ShsMessage shsMessage) {

        DataPart dp = shsMessage.getDataParts().get(0);

        ShsManagement shsManagement = null;
        try {
            shsManagement = marshaller.unmarshal(dp.getDataHandler().getInputStream());
        } catch (Exception e) {
            // TODO decide which exception to throw
            throw new RuntimeException("Failed to unmarshal SHS error message", e);
        }
        if (shsManagement != null) {
            if (shsManagement.getError() != null) {
                Criteria criteria = Criteria.where("label.corrId").is(shsManagement.getCorrId())
                        .and("label.content.contentId").is(shsManagement.getContentId()).and("label.sequenceType")
                        .ne(SequenceType.ADM).and("state").ne(MessageState.QUARANTINED);

                Query query = Query.query(criteria);
                List<ShsMessageEntry> list = mongoTemplate.find(query, ShsMessageEntry.class);
                for (ShsMessageEntry relatedEntry : list) {

                    relatedEntry.setState(MessageState.QUARANTINED);
                    relatedEntry.setStateTimeStamp(new Date());

                    if (shsManagement.getError().getErrorcode() != null) {
                        relatedEntry.setStatusCode(shsManagement.getError().getErrorcode());
                    }
                    if (shsManagement.getError().getErrorinfo() != null) {
                        relatedEntry.setStatusText(shsManagement.getError().getErrorinfo());
                    }

                    update(relatedEntry);
                }
            }
        }

        return shsMessage;
    }

    @Override
    public ShsMessage acknowledgeCorrelatedMessages(ShsMessage shsMessage) {

        DataPart dp = shsMessage.getDataParts().get(0);

        ShsManagement shsManagement = null;
        try {
            shsManagement = marshaller.unmarshal(dp.getDataHandler().getInputStream());
        } catch (Exception e) {
            throw new RuntimeException("Failed to unmarshal SHS confirm message", e);
        }
        if (shsManagement != null) {
            if (shsManagement.getConfirmation() != null) {
                Criteria criteria = Criteria.where("label.corrId").is(shsManagement.getCorrId())
                        .and("label.content.contentId").is(shsManagement.getContentId()).and("label.sequenceType")
                        .ne(SequenceType.ADM);

                Query query = Query.query(criteria);
                List<ShsMessageEntry> list = mongoTemplate.find(query, ShsMessageEntry.class);
                for (ShsMessageEntry relatedEntry : list) {

                    relatedEntry.setAcknowledged(true);
                    update(relatedEntry);
                }
            }
        }

        return shsMessage;
    }

    @Override
    public ShsMessageEntry loadEntry(String shsTo, String txId) {
        ShsMessageEntry entry = messageLogRepository.findOneByLabelTxId(txId);
        if (entry != null && entry.getLabel() != null && entry.getLabel().getTo() != null
                && entry.getLabel().getTo().getValue() != null && entry.getLabel().getTo().getValue().equals(shsTo)
                && !entry.isArchived()) {
            return entry;
        } else {
            throw new MessageNotFoundException("Message entry not found in message log: " + txId);
        }
    }

    @Override
    public ShsMessageEntry update(ShsMessageEntry entry) {
        return messageLogRepository.save(entry);
    }

    @Override
    public ShsMessage loadMessage(ShsMessageEntry entry) {
        ShsMessage message = messageStoreService.findOne(entry);
        if (message == null) {
            MessageNotFoundException e = new MessageNotFoundException(
                    "Message not found in message store: " + entry.getLabel().getTxId());
            messageQuarantined(entry, e);
            throw e;
        }
        return message;
    }

    @Override
    public Iterable<ShsMessageEntry> listMessages(String shsTo, Filter filter) {

        Criteria criteria = Criteria.where("label.to.value").is(shsTo).and("label.transferType")
                .is(TransferType.ASYNCH).and("state").is(MessageState.RECEIVED).and("archived").in(null, false);

        if (filter.getProductIds() != null && !filter.getProductIds().isEmpty()) {
            criteria = criteria.and("label.product.value").in(filter.getProductIds());
        }

        if (filter.getNoAck() == true) {
            criteria = criteria.and("acknowledged").in(false, null);
        }

        if (filter.getStatus() != null) {
            criteria = criteria.and("label.status").is(filter.getStatus());
        }

        if (filter.getEndRecipient() != null) {
            criteria = criteria.and("label.endRecipient.value").is(filter.getEndRecipient());
        }

        if (filter.getOriginator() != null) {
            criteria = criteria.and("label.originatorOrFrom.value").is(filter.getOriginator());
        }

        if (filter.getCorrId() != null) {
            criteria = criteria.and("label.corrId").is(filter.getCorrId());
        }

        if (filter.getContentId() != null) {
            criteria = criteria.and("label.content.contentId").is(filter.getContentId());
        }

        if (filter.getMetaName() != null) {
            criteria = criteria.and("label.meta.name").is(filter.getMetaName());
        }

        if (filter.getMetaValue() != null) {
            criteria = criteria.and("label.meta.value").is(filter.getMetaValue());
        }

        if (filter.getSince() != null) {
            criteria = criteria.and("stateTimeStamp").gte(filter.getSince());
        }

        Query query = Query.query(criteria);

        Sort sort = createAttributeSort(filter);

        Sort arrivalOrderSort = createArrivalOrderSort(filter);

        if (sort != null)
            sort.and(arrivalOrderSort);
        else
            sort = arrivalOrderSort;

        query.with(sort);

        if (filter.getMaxHits() != null && filter.getMaxHits() > 0)
            query = query.limit(filter.getMaxHits());
        else
            query = query.limit(200);

        return mongoTemplate.find(query, ShsMessageEntry.class);
    }

    private Sort createArrivalOrderSort(Filter filter) {
        Sort.Direction arrivalOrderDirection = Sort.Direction.ASC;

        if ("descending".equalsIgnoreCase(filter.getArrivalOrder())) {
            arrivalOrderDirection = Sort.Direction.DESC;
        }

        return new Sort(arrivalOrderDirection, "arrivalTimeStamp");
    }

    private Sort createAttributeSort(Filter filter) {
        Sort.Direction direction = Sort.Direction.ASC;

        if (filter.getSortOrder() == Filter.SortOrder.DESCENDING) {
            direction = Sort.Direction.DESC;
        }

        String sortAttribute = filter.getSortAttribute();

        if (sortAttribute != null) {
            if (sortAttribute.equals("originator")) {
                return new Sort(direction, "label.originatorOrFrom.value");
            } else if (sortAttribute.equals("from")) {
                return new Sort(direction, "label.originatorOrFrom.value");
            } else if (sortAttribute.equals("endrecipient")) {
                return new Sort(direction, "label.endRecipient.value");
            } else if (sortAttribute.equals("producttype")) {
                return new Sort(direction, "label.product.value");
            } else if (sortAttribute.equals("subject")) {
                return new Sort(direction, "label.subject");
            } else if (sortAttribute.equals("contentid")) {
                return new Sort(direction, "label.content.contentId");
            } else if (sortAttribute.equals("-corrid")) {
                return new Sort(direction, "label.corrId");
            } else if (sortAttribute.equals("sequencetype")) {
                return new Sort(direction, "label.sequenceType");
            } else if (sortAttribute.equals("transfertype")) {
                return new Sort(direction, "label.transferType");
            } else if (sortAttribute.startsWith("meta-")) {
                // for now: lets sort on the meta name instead of the meta name's value
                log.warn("Sorting on meta name instead of value corresponding to meta name.");
                return new Sort(direction, "label.meta.name");
            } else {
                throw new IllegalArgumentException("Unsupported sort attribute: " + sortAttribute);
            }
        }

        return null;
    }

    @Override
    public ShsMessageEntry loadEntryAndLockForFetching(String shsTo, String txId) {

        Query query = new Query(Criteria.where("label.txId").is(txId).and("state").is(MessageState.RECEIVED)
                .and("archived").in(false, null).and("label.to.value").is(shsTo));

        Update update = new Update();
        update.set("stateTimeStamp", new Date());
        update.set("state", MessageState.FETCHING_IN_PROGRESS);

        // Enforces that the found object is returned by findAndModify(), i.e. not the original input object
        // It returns null if no document could be updated
        FindAndModifyOptions options = new FindAndModifyOptions().returnNew(true);

        ShsMessageEntry entry = mongoTemplate.findAndModify(query, update, options, ShsMessageEntry.class);

        if (entry == null) {
            throw new MessageNotFoundException("Message entry not found in message log: " + txId);
        }

        return entry;
    }

    @Override
    public int releaseStaleFetchingInProgress() {
        // Anything older than one hour
        Date dateTime = new Date(System.currentTimeMillis() - 3600 * 1000);

        // List all FETCHING_IN_PROGRESS messages
        Query queryList = Query.query(
                Criteria.where("state").is(MessageState.FETCHING_IN_PROGRESS).and("stateTimeStamp").lt(dateTime));
        List<ShsMessageEntry> list = mongoTemplate.find(queryList, ShsMessageEntry.class);

        for (ShsMessageEntry item : list) {

            // Double check that it is still FETCHING_IN_PROGRESS
            Query queryItem = new Query(Criteria.where("label.txId").is(item.getLabel().getTxId()).and("state")
                    .is(MessageState.FETCHING_IN_PROGRESS).and("stateTimeStamp").lt(dateTime));

            Update update = new Update();
            update.set("stateTimeStamp", new Date());
            update.set("state", MessageState.RECEIVED);

            // Enforces that the found object is returned by findAndModify(), i.e. not the original input object
            // It returns null if no document could be updated
            FindAndModifyOptions options = new FindAndModifyOptions().returnNew(true);

            ShsMessageEntry entry = mongoTemplate.findAndModify(queryItem, update, options, ShsMessageEntry.class);

            if (entry != null) {
                log.info("ShsMessageEntry with state FETCHING_IN_PROGRESS moved back to RECEIVED [txId: "
                        + entry.getLabel().getTxId() + "]");
            }
        }

        return list.size();
    }

    @Override
    public int archiveMessages(long messageAgeInSeconds) {

        //check the timestamp for old messages
        Date dateTime = new Date(System.currentTimeMillis() - messageAgeInSeconds * 1000);

        //criteria for automatic archiving
        Query query = new Query();
        query.addCriteria(Criteria.where("arrivalTimeStamp").lt(dateTime).and("archived").in(false, null)
                .orOperator(Criteria.where("state").in("NEW", "SENT", "FETCHED", "QUARANTINED"),
                        Criteria.where("label.transferType").is("SYNCH")));

        //update the archived flag and stateTimestamp value
        Update update = new Update();

        update.set("stateTimeStamp", new Date());
        update.set("archived", true);

        //update all matches in the mongodb
        WriteResult wr = mongoTemplate.updateMulti(query, update, ShsMessageEntry.class);

        log.debug("Archived {} messages modified before {}", wr.getN(), dateTime);
        return wr.getN();
    }

    @Override
    public int removeArchivedMessages(long messageAgeInSeconds) {

        int limit = 1000;
        int totalRemoved = 0;

        //timestamp limit
        Date dateTime = new Date(System.currentTimeMillis() - messageAgeInSeconds * 1000);

        Query query = new Query();
        query.addCriteria(Criteria.where("stateTimeStamp").lt(dateTime).and("archived").is(true));
        //query.with(new Sort(Sort.Direction.DESC, "stateTimeStamp"));

        Boolean moreEntries = false;

        do {
            query.limit(limit);

            List<ShsMessageEntry> entries = mongoTemplate.find(query, ShsMessageEntry.class);
            log.debug("found {} entries", entries.size());

            if (entries.size() > 0 && entries.size() < limit) { //all entries found
                totalRemoved += iterateAndRemove(entries);
                moreEntries = false;

            } else if (entries.size() > 0 && entries.size() == limit) {
                totalRemoved += iterateAndRemove(entries);
                moreEntries = true;
            } else {
                moreEntries = false;
            }
        } while (moreEntries);
        log.debug("Removed {} archived messages modified before {}", totalRemoved, dateTime);

        return totalRemoved;
    }

    @Override
    public int removeSuccessfullyTransferredMessages() {

        int limit = 1000;
        int skip = 0;
        int page = 0;
        int totalRemoved = 0;

        Query query = new Query();
        query.addCriteria(Criteria.where("stateTimeStamp").lt(new Date(System.currentTimeMillis() - 2000))
                .orOperator(Criteria.where("state").is("SENT"),
                        Criteria.where("state").is("RECEIVED").and("label.transferType").is("SYNCH"),
                        Criteria.where("state").is("FETCHED")));
        // query.with(new Sort(Sort.Direction.DESC, "stateTimeStamp"));

        Boolean moreEntries = false;

        do {
            query.limit(limit);
            query.skip(skip);

            List<ShsMessageEntry> entries = mongoTemplate.find(query, ShsMessageEntry.class);
            log.debug("found {} entries", entries.size());

            if (entries.size() > 0 && entries.size() < limit) { //all entries found
                totalRemoved += iterateAndRemove(entries);
                moreEntries = false;

            } else if (entries.size() > 0 && entries.size() == limit) {
                totalRemoved += iterateAndRemove(entries);
                page++;
                skip = page * limit;
                moreEntries = true;
            } else {
                moreEntries = false;
            }

        } while (moreEntries && totalRemoved > 0);

        log.debug("Removed {} transferred messages", totalRemoved);
        return totalRemoved;
    }

    @Override
    public int removeArchivedMessageEntries(long messageAgeInSeconds) {

        int limit = 1000;
        int totalRemoved = 0;

        Date dateTime = new Date(System.currentTimeMillis() - messageAgeInSeconds * 1000);

        Query query = new Query();
        query.addCriteria(Criteria.where("stateTimeStamp").lt(dateTime).and("archived").is(true));
        //query.with(new Sort(Sort.Direction.DESC, "stateTimeStamp"));

        Boolean moreEntries = false;

        do {

            query.limit(limit);

            List<ShsMessageEntry> entries = mongoTemplate.find(query, ShsMessageEntry.class);
            log.debug("found {} entries", entries.size());

            if (entries.size() > 0 && entries.size() < limit) {
                totalRemoved += iterateAndRemoveEntries(entries);
                moreEntries = false;

            } else if (entries.size() > 0 && entries.size() == limit) {
                totalRemoved += iterateAndRemoveEntries(entries);
                moreEntries = true;
            } else {
                moreEntries = false;
            }

        } while (moreEntries);

        log.debug("Removed {} archived messageEntries modified before {}", totalRemoved, dateTime);

        return totalRemoved;
    }

    private int iterateAndRemove(List<ShsMessageEntry> entries) {

        int removed = 0;
        for (int i = 0; i < entries.size(); i++) {
            if (messageStoreService.exists(entries.get(i))) {
                messageStoreService.delete(entries.get(i));
                removed++;
                log.debug("removed a message {}", entries.get(i));
            }
        }
        return removed;
    }

    private int iterateAndRemoveEntries(List<ShsMessageEntry> entries) {

        int removed = 0;
        for (int i = 0; i < entries.size(); i++) {
            if (!messageStoreService.exists(entries.get(i))) {
                deleteShsMessageEntry(entries.get(i));
                removed++;
                log.debug("removed a message entry {}", entries.get(i));
            }
        }
        return removed;
    }
}