com.auditbucket.engine.service.TrackService.java Source code

Java tutorial

Introduction

Here is the source code for com.auditbucket.engine.service.TrackService.java

Source

/*
 * Copyright (c) 2012-2014 "Monowai Developments Limited"
 *
 * This file is part of AuditBucket.
 *
 * AuditBucket is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * AuditBucket 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with AuditBucket.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.auditbucket.engine.service;

import com.auditbucket.dao.TrackDao;
import com.auditbucket.helper.DatagioException;
import com.auditbucket.helper.SecurityHelper;
import com.auditbucket.registration.model.Company;
import com.auditbucket.registration.model.Fortress;
import com.auditbucket.registration.model.FortressUser;
import com.auditbucket.registration.model.SystemUser;
import com.auditbucket.registration.service.*;
import com.auditbucket.search.model.MetaSearchChange;
import com.auditbucket.track.bean.*;
import com.auditbucket.track.model.*;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.hibernate.validator.constraints.NotEmpty;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
 *
 * Transactional services to support record and working with headers and logs
 *
 * User: Mike Holdsworth
 * Date: 8/04/13
 *
 */

@Service(value = "ab.TrackerService")
@Transactional
public class TrackService {
    private static final String EMPTY = "";
    @Autowired
    FortressService fortressService;

    @Autowired
    CompanyService companyService;

    @Autowired
    TrackEventService trackEventService;

    @Autowired
    SystemUserService sysUserService;

    @Autowired
    TagTrackService tagTrackService;

    @Autowired
    WhatService whatService;

    @Autowired
    TrackDao trackDao;

    @Autowired
    TagService tagService;

    @Autowired
    private SecurityHelper securityHelper;

    @Autowired
    private SearchServiceFacade searchFacade;

    private Logger logger = LoggerFactory.getLogger(TrackService.class);

    @Autowired
    private KeyGenService keyGenService;

    public LogWhat getWhat(MetaHeader metaHeader, ChangeLog change) {
        return whatService.getWhat(metaHeader, change);
    }

    TxRef beginTransaction(Company company) {
        return beginTransaction(keyGenService.getUniqueKey(), company);
    }

    TxRef beginTransaction(String id, Company company) {
        return trackDao.beginTransaction(id, company);

    }

    public Map<String, Object> findByTXRef(String txRef) {
        TxRef tx = findTx(txRef);
        return (tx == null ? null : trackDao.findByTransaction(tx));
    }

    /**
     * Creates a fortressName specific metaHeader for the caller. FortressUserNode is automatically
     * created if it does not exist.
     *
     * @return unique primary key to be used for subsequent log calls
     */
    public TrackResultBean createHeader(Company company, Fortress fortress, MetaInputBean inputBean) {
        DocumentType documentType;
        documentType = tagService.resolveDocType(fortress, inputBean.getDocumentType());

        // Create thisFortressUser if missing
        FortressUser fu = fortressService.getFortressUser(fortress, inputBean.getFortressUser(), true);
        fu.setFortress(fortress);// Save fetching it twice

        MetaHeader ah = null;
        if (inputBean.getCallerRef() != null && !inputBean.getCallerRef().equals(EMPTY))
            ah = findByCallerRef(fortress, documentType, inputBean.getCallerRef());

        if (ah != null) {
            logger.debug("Existing metaHeader record found by Caller Ref [{}] found [{}]", inputBean.getCallerRef(),
                    ah.getMetaKey());
            inputBean.setMetaKey(ah.getMetaKey());

            TrackResultBean arb = new TrackResultBean(ah);
            arb.setMetaInputBean(inputBean);
            arb.setWasDuplicate();
            // Could be rewriting tags
            tagTrackService.associateTags(company, ah, inputBean.getTags());

            return arb;
        }

        try {

            ah = makeHeader(inputBean, fu, documentType);
        } catch (DatagioException e) {
            logger.error(e.getMessage());
            return new TrackResultBean("Error processing inputBean [{}]" + inputBean + ". Error " + e.getMessage());
        }
        TrackResultBean resultBean = new TrackResultBean(ah);
        resultBean.setMetaInputBean(inputBean);
        if (inputBean.isTrackSuppressed())
            // We need to get the "tags" across to ElasticSearch, so we mock them ;)
            resultBean.setTags(
                    tagTrackService.associateTags(company, resultBean.getMetaHeader(), inputBean.getTags()));
        else
            // Write the associations to the graph
            tagTrackService.associateTags(company, resultBean.getMetaHeader(), inputBean.getTags());

        resultBean.setLogInput(inputBean.getLog());
        return resultBean;

    }

    private MetaHeader makeHeader(MetaInputBean inputBean, FortressUser fu, DocumentType documentType)
            throws DatagioException {
        inputBean.setMetaKey(keyGenService.getUniqueKey());
        MetaHeader ah = trackDao.create(inputBean, fu, documentType);
        if (ah.getId() == null)
            inputBean.setMetaKey("NT " + fu.getFortress().getFortressKey()); // We ain't tracking this
        logger.debug("Meta Header created:{} key=[{}]", ah.getId(), ah.getMetaKey());
        return ah;
    }

    /**
     * When you have no API key, find if authorised
     * @param metaKey known GUID
     * @return header the caller is authorised to view
     */
    public MetaHeader getHeader(@NotEmpty String metaKey) {
        String userName = securityHelper.getLoggedInUser();
        SystemUser su = sysUserService.findByName(userName);
        if (su == null)
            throw new SecurityException(userName + "Not authorised to retrieve headers");

        return getHeader(su.getCompany(), metaKey, false);
    }

    public MetaHeader getHeader(Company company, String metaKey) {
        if (company == null && metaKey != null)
            return getHeader(metaKey); // we could find by basicauth
        if (company == null)
            return null;

        return getHeader(company, metaKey, false);

    }

    public MetaHeader getHeader(Company company, @NotEmpty String headerKey, boolean inflate) {

        if (company == null)
            return getHeader(headerKey);
        MetaHeader ah = trackDao.findHeader(headerKey, inflate);
        if (ah == null)
            return null;

        if (!(ah.getFortress().getCompany().getId().equals(company.getId())))
            throw new SecurityException(
                    "CompanyNode mismatch. [" + headerKey + "] working for [" + company.getName()
                            + "] cannot write meta records for [" + ah.getFortress().getCompany().getName() + "]");
        return ah;
    }

    /**
     * Looks up the metaHeader from input and creates a log record
     * <p/>
     * Only public to support AOP transactions. You should be calling this via #MediationFacade.createLog
     *
     * @param input log details
     * @return populated log information with any error messages
     */
    public LogResultBean createLog(MetaHeader header, LogInputBean input) throws DatagioException {
        LogResultBean resultBean = new LogResultBean(input);

        if (header == null) {
            String metaKey = input.getMetaKey();
            if (input.getMetaId() == null) {
                if (metaKey == null || metaKey.equals(EMPTY)) {
                    header = findByCallerRef(input.getFortress(), input.getDocumentType(), input.getCallerRef());
                    if (header != null)
                        input.setMetaKey(header.getMetaKey());
                } else
                    header = getHeader(metaKey); // true??
            } else
                header = getHeader(input.getMetaId()); // Only set internally by AuditBucket. Never rely on the caller
        }

        if (header == null) {
            resultBean.setStatus(LogInputBean.LogStatus.NOT_FOUND);
            resultBean.setMessage("Unable to locate requested header");
            return resultBean;
        }
        FortressUser thisFortressUser = fortressService.getFortressUser(header.getFortress(),
                input.getFortressUser(), true);
        return createLog(header, input, thisFortressUser);
    }

    /**
     * Event log record for the supplied metaHeader from the supplied input
     *
     * @param authorisedHeader metaHeader the caller is authorised to work with
     * @param input            trackLog details containing the data to log
     * @param thisFortressUser User name in calling system that is making the change
     * @return populated log information with any error messages
     */
    private LogResultBean createLog(MetaHeader authorisedHeader, LogInputBean input, FortressUser thisFortressUser)
            throws DatagioException {
        // Warning - making this private means it doesn't get a transaction!
        LogResultBean resultBean = new LogResultBean(input);
        //ToDo: May want to track a "View" event which would not change the What data.
        if (input.getMapWhat() == null || input.getMapWhat().isEmpty()) {
            resultBean.setStatus(LogInputBean.LogStatus.IGNORE);
            resultBean.setMessage("No 'what' information provided. Ignoring this request");
            return resultBean;
        }

        Fortress fortress = authorisedHeader.getFortress();

        // Transactions checks
        TxRef txRef = handleTxRef(input, fortress.getCompany());
        resultBean.setTxReference(txRef);

        // https://github.com/monowai/auditbucket/issues/7
        TrackLog existingLog = null;
        if (authorisedHeader.getLastUpdated() != authorisedHeader.getWhenCreated()) // Will there even be a change to find
            existingLog = getLastLog(authorisedHeader);

        Boolean searchActive = fortress.isSearchActive();
        DateTime fortressWhen = (input.getWhen() == null ? new DateTime(DateTimeZone.forID(fortress.getTimeZone()))
                : new DateTime(input.getWhen()));

        if (existingLog != null) {
            try {
                if (whatService.isSame(authorisedHeader, existingLog.getChange(), input.getWhat())) {
                    logger.trace("Ignoring a change we already have {}", input);
                    input.setStatus(LogInputBean.LogStatus.IGNORE);
                    if (input.isForceReindex()) { // Caller is recreating the search index
                        searchFacade.makeChangeSearchable(prepareSearchDocument(authorisedHeader, input,
                                existingLog.getChange().getEvent(), searchActive, fortressWhen, existingLog));
                        resultBean.setMessage("Ignoring a change we already have. Honouring request to re-index");
                    } else
                        resultBean.setMessage("Ignoring a change we already have");
                    return resultBean;
                }
            } catch (IOException e) {
                input.setStatus(LogInputBean.LogStatus.ILLEGAL_ARGUMENT);
                resultBean.setMessage("Error comparing JSON data: " + e.getMessage());
                logger.error("Error comparing JSON Data", e);
                return resultBean;
            }
            if (input.getEvent() == null) {
                input.setEvent(ChangeLog.UPDATE);
            }
            if (searchActive)
                authorisedHeader = waitOnInitialSearchResult(authorisedHeader);

        } else { // first ever log for the metaHeader
            if (input.getEvent() == null) {
                input.setEvent(ChangeLog.CREATE);
            }
            //if (!metaHeader.getLastUser().getId().equals(thisFortressUser.getId())){
            authorisedHeader.setLastUser(thisFortressUser);
            authorisedHeader = trackDao.save(authorisedHeader);

            //}
        }

        ChangeLog thisChange = trackDao.save(thisFortressUser, input, txRef,
                (existingLog != null ? existingLog.getChange() : null));
        input.setChangeEvent(thisChange.getEvent());

        // ToDo: WhatService call should occur after this function is finished.
        //       change should then be written back to the graph via @ServiceActivator as called
        //       by as yet to be extracted ab-what service
        thisChange = whatService.logWhat(authorisedHeader, thisChange, input.getWhat());

        TrackLog newLog = trackDao.addLog(authorisedHeader, thisChange, fortressWhen, existingLog);
        resultBean.setSysWhen(newLog.getSysWhen());

        boolean moreRecent = (existingLog == null || existingLog.getFortressWhen() <= newLog.getFortressWhen());

        input.setStatus(LogInputBean.LogStatus.OK);

        if (moreRecent) {
            if (!authorisedHeader.getLastUser().getId().equals(thisFortressUser.getId())) {
                authorisedHeader.setLastUser(thisFortressUser);
                trackDao.save(authorisedHeader);
            }

            try {
                resultBean.setSearchChange(prepareSearchDocument(authorisedHeader, input, input.getChangeEvent(),
                        searchActive, fortressWhen, newLog));
            } catch (JsonProcessingException e) {
                resultBean.setMessage("Error processing JSON document");
                resultBean.setStatus(LogInputBean.LogStatus.ILLEGAL_ARGUMENT);
            }
        }

        return resultBean;

    }

    private static final ObjectMapper om = new ObjectMapper();

    public SearchChange prepareSearchDocument(MetaHeader metaHeader, LogInputBean logInput, ChangeEvent event,
            Boolean searchActive, DateTime fortressWhen, TrackLog trackLog) throws JsonProcessingException {

        if (!searchActive || metaHeader.isSearchSuppressed())
            return null;
        SearchChange searchDocument;
        searchDocument = new MetaSearchChange(metaHeader, logInput.getMapWhat(), event.getCode(), fortressWhen);
        searchDocument.setWho(trackLog.getChange().getWho().getCode());
        searchDocument.setTags(tagTrackService.findTrackTags(metaHeader.getFortress().getCompany(), metaHeader));
        searchDocument.setDescription(metaHeader.getName());
        try {
            logger.trace("JSON {}", om.writeValueAsString(searchDocument));
        } catch (JsonProcessingException e) {
            logger.error(e.getMessage());
            throw (e);
        }
        if (trackLog != null && trackLog.getSysWhen() != 0)
            searchDocument.setSysWhen(trackLog.getSysWhen());
        else
            searchDocument.setSysWhen(metaHeader.getWhenCreated());

        // Used to reconcile that the change was actually indexed
        logger.trace("Preparing Search Document [{}]", trackLog);
        searchDocument.setLogId(trackLog.getId());
        return searchDocument;
    }

    public Collection<MetaHeader> getHeaders(Fortress fortress, Long skipTo) {
        return trackDao.findHeaders(fortress.getId(), skipTo);
    }

    public Collection<MetaHeader> getHeaders(Fortress fortress, String docTypeName, Long skipTo) {
        DocumentType docType = tagService.resolveDocType(fortress, docTypeName);
        return trackDao.findHeaders(fortress.getId(), docType.getName(), skipTo);
    }

    private MetaHeader waitOnInitialSearchResult(MetaHeader metaHeader) {

        if (metaHeader.isSearchSuppressed() || metaHeader.getSearchKey() != null)
            return metaHeader; // Nothing to wait for as we're suppressing searches for this metaHeader

        int timeOut = 100;
        int i = 0;

        while (metaHeader.getSearchKey() == null && i < timeOut) {
            i++;
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                logger.error(e.getMessage());
            }
            metaHeader = getHeader(metaHeader.getId());
        }
        if (metaHeader.getSearchKey() == null)
            logger.error("Timeout waiting for the initial search document to be created [{}]",
                    metaHeader.getMetaKey());
        return metaHeader;

    }

    private MetaHeader getHeader(Long id) {
        return trackDao.getHeader(id);
    }

    private TxRef handleTxRef(LogInputBean input, Company company) {
        TxRef txRef = null;
        if (input.isTransactional()) {
            if (input.getTxRef() == null) {
                txRef = beginTransaction(company);
                input.setTxRef(txRef.getName());
            } else {
                txRef = beginTransaction(input.getTxRef(), company);
            }
        }

        return txRef;
    }

    public TxRef findTx(String txRef) {
        return findTx(txRef, false);
    }

    TxRef findTx(String txRef, boolean fetchHeaders) {
        String userName = securityHelper.getLoggedInUser();
        SystemUser su = sysUserService.findByName(userName);

        if (su == null)
            throw new SecurityException("Not authorised");
        TxRef tx = trackDao.findTxTag(txRef, su.getCompany(), fetchHeaders);
        if (tx == null)
            return null;
        return tx;
    }

    public Set<MetaHeader> findTxHeaders(String txName) {
        TxRef txRef = findTx(txName);
        if (txRef == null)
            return null;
        return trackDao.findHeadersByTxRef(txRef.getId());
    }

    public void updateHeader(MetaHeader metaHeader) {
        trackDao.save(metaHeader);
    }

    public TrackLog getLastLog(String metaKey) throws DatagioException {
        MetaHeader header = getValidHeader(metaKey);
        return getLastLog(header.getId());

    }

    public TrackLog getLastLog(Company company, String metaKey) throws DatagioException {
        MetaHeader header = getHeader(company, metaKey);
        return getLastLog(header);
    }

    public TrackLog getLastLog(MetaHeader metaHeader) throws DatagioException {
        logger.debug("Getting lastLog MetaID [{}]", metaHeader.getId());
        return trackDao.getLastLog(metaHeader.getId());
    }

    TrackLog getLastLog(Long headerId) {
        return trackDao.getLastLog(headerId);
    }

    public Set<TrackLog> getLogs(Long headerId) {
        return trackDao.getLogs(headerId);
    }

    public Set<TrackLog> getLogs(Company company, String headerKey) throws DatagioException {
        MetaHeader metaHeader = getHeader(company, headerKey);
        return trackDao.getLogs(metaHeader.getId());
    }

    public Set<TrackLog> getLogs(String headerKey, Date from, Date to) throws DatagioException {
        MetaHeader metaHeader = getValidHeader(headerKey);
        return getLogs(metaHeader, from, to);
    }

    Set<TrackLog> getLogs(MetaHeader metaHeader, Date from, Date to) {
        return trackDao.getLogs(metaHeader.getId(), from, to);
    }

    /**
     * blocks until the header has been cancelled
     *
     * @param headerKey UID of the Header
     * @return LogResultBean
     * @throws IOException
     */
    public MetaHeader cancelLastLogSync(String headerKey) throws IOException, DatagioException {
        Future<MetaHeader> futureHeader = cancelLastLog(headerKey);
        try {
            return futureHeader.get();
        } catch (InterruptedException | ExecutionException e) {
            logger.error(e.getMessage());
            throw new DatagioException("This is bad - Interrupted Exception ", e);
        }
    }

    /**
     * This could be used toa assist in compensating transactions to roll back the last change
     * if the caller decides a rollback is required after the log has been written.
     * If there are no ChangeLog records left, then the metaHeader will also be removed and the
     * AB headerKey will be forever invalid.
     *
     * @param headerKey UID of the metaHeader
     * @return Future<LogResultBean> record or null if no metaHeader exists.
     */
    @Async
    Future<MetaHeader> cancelLastLog(String headerKey) throws IOException, DatagioException {
        MetaHeader metaHeader = getValidHeader(headerKey, true);
        TrackLog currentLog = getLastLog(metaHeader.getId());
        if (currentLog == null)
            return null;
        trackDao.fetch(currentLog.getChange());
        ChangeLog currentChange = currentLog.getChange();
        ChangeLog priorChange = currentLog.getChange().getPreviousChange();

        if (priorChange != null) {
            trackDao.makeLastChange(metaHeader, priorChange);
            trackDao.fetch(priorChange);
            metaHeader.setLastUser(
                    fortressService.getFortressUser(metaHeader.getFortress(), priorChange.getWho().getCode()));
            metaHeader = trackDao.save(metaHeader);
            whatService.delete(metaHeader, currentChange);
            trackDao.delete(currentChange);
        } else if (currentChange != null) {
            // No changes left, there is now just a header
            // What to to? Delete the metaHeader? Store the "canceled By" User? Assign the log to a Cancelled RLX?
            // ToDo: Delete from ElasticSearch??
            metaHeader.setLastUser(
                    fortressService.getFortressUser(metaHeader.getFortress(), metaHeader.getCreatedBy().getCode()));
            metaHeader = trackDao.save(metaHeader);
            whatService.delete(metaHeader, currentChange);
            trackDao.delete(currentChange);
        }

        if (priorChange == null)
            // Nothing to index, no changes left so we're done
            return new AsyncResult<>(metaHeader);

        // Sync the update to ab-search.
        if (metaHeader.getFortress().isSearchActive() && !metaHeader.isSearchSuppressed()) {
            // Update against the MetaHeader only by re-indexing the search document
            Map<String, Object> priorWhat = whatService.getWhat(metaHeader, priorChange).getWhatMap();

            MetaSearchChange searchDocument = new MetaSearchChange(metaHeader, priorWhat,
                    priorChange.getEvent().getCode(), new DateTime(priorChange.getLog().getFortressWhen()));
            searchDocument.setTags(tagTrackService.findTrackTags(metaHeader));
            searchFacade.makeChangeSearchable(searchDocument);
        }
        return new AsyncResult<>(metaHeader);
    }

    /**
     * counts the number of logs that exist for the given metaHeader
     *
     * @param headerKey GUID
     * @return count
     */
    public int getLogCount(String headerKey) throws DatagioException {
        MetaHeader metaHeader = getValidHeader(headerKey);
        return trackDao.getLogCount(metaHeader.getId());

    }

    private MetaHeader getValidHeader(String headerKey) throws DatagioException {
        return getValidHeader(headerKey, false);
    }

    private MetaHeader getValidHeader(String headerKey, boolean inflate) throws DatagioException {
        MetaHeader header = trackDao.findHeader(headerKey, inflate);
        if (header == null) {
            throw new DatagioException("No metaHeader for [" + headerKey + "]");
        }
        String userName = securityHelper.getLoggedInUser();
        SystemUser sysUser = sysUserService.findByName(userName);

        if (!header.getFortress().getCompany().getId().equals(sysUser.getCompany().getId())) {
            throw new SecurityException("Not authorised to work with this meta data");
        }
        return header;

    }

    public MetaHeader findByCallerRef(String fortress, String documentType, String callerRef) {
        Fortress iFortress = fortressService.findByName(fortress);
        if (iFortress == null)
            return null;

        return findByCallerRef(iFortress, documentType, callerRef);
    }

    public MetaHeader findByCallerRefFull(Long fortressId, String documentType, String callerRef) {
        Fortress fortress = fortressService.getFortress(fortressId);
        return findByCallerRefFull(fortress, documentType, callerRef);

    }

    /**
     * \
     * inflates the search result with dependencies populated
     *
     * @param fortress     System
     * @param documentType Class of doc
     * @param callerRef    fortressName PK
     * @return inflated header
     */
    public MetaHeader findByCallerRefFull(Fortress fortress, String documentType, String callerRef) {
        return findByCallerRef(fortress, documentType, callerRef);
    }

    /**
     * Locates all the MetaHeaders irrespective of the document type. Use this when you know that that callerRef is
     * unique for the entire fortressName
     *
     * @param company       Company you are authorised to work with
     * @param fortressName  Fortress to restrict the search to
     * @param callerRef     key to locate
     *
     * @return metaHeaders
     */
    public Iterable<MetaHeader> findByCallerRef(Company company, String fortressName, String callerRef) {
        Fortress fortress = fortressService.findByName(company, fortressName);
        return findByCallerRef(fortress, callerRef);
    }

    public Iterable<MetaHeader> findByCallerRef(Fortress fortress, String callerRef) {
        return trackDao.findByCallerRef(fortress.getId(), callerRef.trim());
    }

    public MetaHeader findByCallerRef(Fortress fortress, String documentType, String callerRef) {
        DocumentType doc = tagService.resolveDocType(fortress, documentType, false);
        if (doc == null)
            return null;
        return findByCallerRef(fortress, doc, callerRef);

    }

    /**
     * @param fortress owning system
     * @param documentType class of document
     * @param callerRef    fortressName primary key
     * @return LogResultBean or NULL.
     */
    public MetaHeader findByCallerRef(Fortress fortress, DocumentType documentType, String callerRef) {
        return trackDao.findByCallerRef(fortress.getId(), documentType.getId(), callerRef.trim());
    }

    public TrackedSummaryBean getMetaSummary(Company company, String metaKey) throws DatagioException {
        MetaHeader header = getHeader(company, metaKey, true);
        if (header == null)
            throw new DatagioException("Invalid Meta Key [" + metaKey + "]");
        Set<TrackLog> changes = getLogs(header.getId());
        Set<TrackTag> tags = tagTrackService.findTrackTags(company, header);
        return new TrackedSummaryBean(header, changes, tags);
    }

    public LogDetailBean getFullDetail(String metaKey, Long logId) {
        Company company = securityHelper.getCompany();
        return getFullDetail(company, metaKey, logId);
    }

    public LogDetailBean getFullDetail(Company company, String metaKey, Long logId) {
        MetaHeader metaHeader = getHeader(company, metaKey, true);
        if (metaHeader == null)
            return null;

        TrackLog log = trackDao.getLog(logId);
        trackDao.fetch(log.getChange());
        LogWhat what = whatService.getWhat(metaHeader, log.getChange());
        log.getChange().setWhat(what);
        return new LogDetailBean(log, what);
    }

    public TrackLog getLogForHeader(MetaHeader header, Long logId) {
        if (header != null) {

            TrackLog log = trackDao.getLog(logId);
            if (!log.getMetaHeader().getId().equals(header.getId()))
                return null;

            trackDao.fetch(log.getChange());
            return log;
        }
        return null;
    }

    public Iterable<TrackResultBean> createHeaders(Iterable<MetaInputBean> inputBeans, Company company,
            Fortress fortress) {
        Collection<TrackResultBean> arb = new CopyOnWriteArrayList<>();
        for (MetaInputBean inputBean : inputBeans) {
            logger.trace("Batch Processing callerRef=[{}], documentType=[{}]", inputBean.getCallerRef(),
                    inputBean.getDocumentType());
            arb.add(createHeader(company, fortress, inputBean));
        }
        return arb;

    }

    /**
     * Cross references to meta headers to create a link
     *
     * @param company   validated company the caller is authorised to work with
     * @param metaKey   source from which a xref will be created
     * @param xRef      target for the xref
     * @param reference name of the relationship
     */
    public Collection<String> crossReference(Company company, String metaKey, Collection<String> xRef,
            String reference) throws DatagioException {
        MetaHeader header = getHeader(company, metaKey);
        if (header == null) {
            throw new DatagioException(
                    "Unable to find the Meta Header [" + metaKey + "]. Perhaps it has not been processed yet?");
        }
        Collection<MetaHeader> targets = new ArrayList<>();
        Collection<String> ignored = new ArrayList<>();
        for (String next : xRef) {
            MetaHeader m = getHeader(company, next);
            if (m != null) {
                targets.add(m);
            } else {
                ignored.add(next);
                //logger.info ("Unable to find MetaKey ["+metaKey+"]. Skipping");
            }
        }
        trackDao.crossReference(header, targets, reference);
        return ignored;
    }

    public Map<String, Collection<MetaHeader>> getCrossReference(Company company, String metaKey, String xRefName)
            throws DatagioException {
        MetaHeader header = getHeader(company, metaKey);
        if (header == null) {
            throw new DatagioException(
                    "Unable to find the Meta Header [" + metaKey + "]. Perhaps it has not been processed yet?");
        }

        return trackDao.getCrossReference(company, header, xRefName);
    }

    public Map<String, Collection<MetaHeader>> getCrossReference(Company company, String fortressName,
            String callerRef, String xRefName) throws DatagioException {
        Fortress fortress = fortressService.findByName(company, fortressName);

        MetaHeader source = trackDao.findByCallerRefUnique(fortress.getId(), callerRef);
        if (source == null) {
            throw new DatagioException("Unable to find the Meta Header [" + callerRef + "]");
        }

        return trackDao.getCrossReference(company, source, xRefName);
    }

    public List<String> crossReferenceByCallerRef(Company company, String fortressName, String sourceKey,
            Collection<String> callerRefs, String xRefName) throws DatagioException {
        Fortress f = fortressService.findByName(company, fortressName);
        MetaHeader header = trackDao.findByCallerRefUnique(f.getId(), sourceKey);
        if (header == null)
            throw new DatagioException("Unable to locate the MetaHeader for CallerRef [" + sourceKey
                    + "]\" in the Fortress [" + fortressName + "]");

        //16051954
        Collection<MetaHeader> targets = new ArrayList<>();
        List<String> ignored = new ArrayList<>();

        for (String callerRef : callerRefs) {
            int count = 1;
            Iterable<MetaHeader> metaHeaders = findByCallerRef(f, callerRef);
            for (MetaHeader metaHeader : metaHeaders) {
                if (count > 1 || count == 0)
                    ignored.add(callerRef);
                else
                    targets.add(metaHeader);
                count++;
            }

        }
        trackDao.crossReference(header, targets, xRefName);
        return ignored;
    }

    public Collection<MetaHeader> getHeaders(Company company, Collection<String> toFind) {
        return trackDao.findHeaders(company, toFind);
    }
}