org.zanata.service.impl.TranslationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.zanata.service.impl.TranslationServiceImpl.java

Source

/*
 * Copyright 2012, Red Hat, Inc. and individual contributors as indicated by the
 * @author tags. See the copyright.txt file in the distribution for a full
 * listing of individual contributors.
 *
 * This 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 2.1 of the License, or (at your option)
 * any later version.
 *
 * This software 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 software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package org.zanata.service.impl;

import static org.zanata.transaction.TransactionUtilImpl.runInTransaction;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.annotation.Nonnull;
import javax.enterprise.context.RequestScoped;
import javax.persistence.EntityManager;
import com.google.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.HibernateException;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.deltaspike.jpa.api.transaction.Transactional;
import org.zanata.async.Async;
import org.zanata.async.AsyncTaskHandle;
import org.zanata.async.AsyncTaskResult;
import org.zanata.common.ContentState;
import org.zanata.common.LocaleId;
import org.zanata.common.MergeType;
import org.zanata.common.util.ContentStateUtil;
import org.zanata.dao.DocumentDAO;
import org.zanata.dao.ProjectIterationDAO;
import org.zanata.dao.TextFlowDAO;
import org.zanata.dao.TextFlowTargetDAO;
import org.zanata.events.DocStatsEvent;
import org.zanata.events.DocumentLocaleKey;
import org.zanata.events.DocumentUploadedEvent;
import org.zanata.events.TextFlowTargetStateEvent;
import org.zanata.exception.ZanataServiceException;
import org.zanata.i18n.Messages;
import org.zanata.lock.Lock;
import org.zanata.model.HAccount;
import org.zanata.model.HDocument;
import org.zanata.model.HLocale;
import org.zanata.model.HPerson;
import org.zanata.model.HProjectIteration;
import org.zanata.model.HSimpleComment;
import org.zanata.model.HTextFlow;
import org.zanata.model.HTextFlowTarget;
import org.zanata.model.HTextFlowTargetHistory;
import org.zanata.model.type.EntityType;
import org.zanata.model.type.TranslationSourceType;
import org.zanata.rest.dto.resource.TextFlowTarget;
import org.zanata.rest.dto.resource.TranslationsResource;
import org.zanata.rest.service.ResourceUtils;
import org.zanata.security.ZanataIdentity;
import org.zanata.security.annotations.Authenticated;
import org.zanata.service.LocaleService;
import org.zanata.service.LockManagerService;
import org.zanata.service.TranslationMergeService;
import org.zanata.service.TranslationService;
import org.zanata.service.ValidationService;
import javax.enterprise.event.Event;
import org.zanata.util.ShortString;
import org.zanata.webtrans.shared.model.TransUnitId;
import org.zanata.webtrans.shared.model.TransUnitUpdateInfo;
import org.zanata.webtrans.shared.model.TransUnitUpdateRequest;
import org.zanata.webtrans.shared.model.ValidationAction;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import static org.zanata.events.TextFlowTargetStateEvent.TextFlowTargetStateChange;

@Named("translationServiceImpl")
@RequestScoped
public class TranslationServiceImpl implements TranslationService {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TranslationServiceImpl.class);
    private static final long serialVersionUID = -4409365256354251534L;

    @SuppressFBWarnings(value = "SE_BAD_FIELD")
    @Inject
    private EntityManager entityManager;
    @Inject
    private ProjectIterationDAO projectIterationDAO;
    @Inject
    private DocumentDAO documentDAO;
    @Inject
    private TextFlowDAO textFlowDAO;
    @Inject
    private TextFlowTargetDAO textFlowTargetDAO;
    @Inject
    private ResourceUtils resourceUtils;
    @Inject
    private LocaleService localeServiceImpl;
    @Inject
    private LockManagerService lockManagerServiceImpl;
    @Inject
    private ValidationService validationServiceImpl;
    @Inject
    @Authenticated
    private HAccount authenticatedAccount;
    @Inject
    private ZanataIdentity identity;
    @Inject
    private TranslationMergeServiceFactory translationMergeServiceFactory;
    @Inject
    private Messages msgs;
    @SuppressFBWarnings(value = "SE_BAD_FIELD")
    @Inject
    private Event<DocumentUploadedEvent> documentUploadedEvent;
    @SuppressFBWarnings(value = "SE_BAD_FIELD")
    @Inject
    private Event<TextFlowTargetStateEvent> textFlowTargetStateEvent;
    @SuppressFBWarnings(value = "SE_BAD_FIELD")
    @Inject
    private Event<DocStatsEvent> docStatsEvent;

    @Transactional
    @Override
    public List<TranslationResult> translate(LocaleId localeId, List<TransUnitUpdateRequest> translationRequests) {
        return translate(localeId, translationRequests, true);
    }

    /**
     * This is used when reverting translation
     *
     * @param localeId
     * @param translationRequests
     * @return
     */
    private List<TranslationResult> translateWithoutValidating(LocaleId localeId,
            List<TransUnitUpdateRequest> translationRequests) {
        return translate(localeId, translationRequests, false);
    }

    private List<TranslationResult> translate(LocaleId localeId, List<TransUnitUpdateRequest> translationRequests,
            boolean runValidation) {
        List<TranslationResult> results = new ArrayList<TranslationResult>();
        // avoid locale check if there is nothing to translate
        if (translationRequests.isEmpty()) {
            return results;
        }
        // single locale check - assumes update requests are all from the same
        // project-iteration
        HTextFlow sampleHTextFlow = entityManager.find(HTextFlow.class,
                translationRequests.get(0).getTransUnitId().getValue());
        HProjectIteration projectIteration = sampleHTextFlow.getDocument().getProjectIteration();
        HLocale hLocale = validateLocale(localeId, projectIteration);
        // single permission check - assumes update requests are all from same
        // project
        validateReviewPermissionIfApplicable(translationRequests, projectIteration, hLocale);
        List<TextFlowTargetStateChange> targetStates = Lists.newArrayList();
        Map<ContentState, Long> contentStateDeltas = Maps.newHashMap();
        for (TransUnitUpdateRequest request : translationRequests) {
            HTextFlow hTextFlow = entityManager.find(HTextFlow.class, request.getTransUnitId().getValue());
            TranslationResultImpl result = new TranslationResultImpl();
            if (runValidation) {
                String validationMessage = validateTranslations(request.getNewContentState(), projectIteration,
                        request.getTransUnitId().toString(), hTextFlow.getContents(), request.getNewContents());
                if (!StringUtils.isEmpty(validationMessage)) {
                    log.warn(validationMessage);
                    result.isSuccess = false;
                    result.errorMessage = validationMessage;
                    results.add(result);
                    continue;
                }
            }
            HTextFlowTarget hTextFlowTarget = textFlowTargetDAO.getOrCreateTarget(hTextFlow, hLocale);
            // if hTextFlowTarget is created, any further hibernate fetch will
            // trigger an implicit flush
            // (which will save this target even if it's not fully ready!!!)
            if (request.hasTargetComment()) {
                // FIXME this creates orphan comments, and replaces identical
                // comments with copies
                hTextFlowTarget.setComment(new HSimpleComment(request.getTargetComment()));
            }
            result.baseVersion = hTextFlowTarget.getVersionNum();
            result.baseContentState = hTextFlowTarget.getState();
            if (request.getBaseTranslationVersion() == hTextFlowTarget.getVersionNum()) {
                try {
                    int nPlurals = getNumPlurals(hLocale, hTextFlow);
                    ContentState currentState = hTextFlowTarget.getState();
                    result.targetChanged = translate(hTextFlowTarget, request.getNewContents(),
                            request.getNewContentState(), nPlurals, new TranslationDetails(request));
                    // fire event after flush
                    if (result.targetChanged || hTextFlowTarget.getVersionNum() == 0) {
                        HTextFlow textFlow = hTextFlowTarget.getTextFlow();
                        aggregateChanges(textFlow, hTextFlowTarget, currentState, targetStates, contentStateDeltas);
                    }
                    result.isSuccess = true;
                } catch (HibernateException e) {
                    result.isSuccess = false;
                    log.warn("HibernateException while translating");
                }
            } else {
                // concurrent edits not allowed
                String errorMessage = "translation failed for textflow " + hTextFlow.getId()
                        + ": base versionNum + " + request.getBaseTranslationVersion()
                        + " does not match current versionNum " + hTextFlowTarget.getVersionNum();
                log.warn(errorMessage);
                result.errorMessage = errorMessage;
                result.isVersionNumConflict = true;
                result.isSuccess = false;
            }
            result.translatedTextFlowTarget = hTextFlowTarget;
            results.add(result);
        }
        if (!targetStates.isEmpty()) {
            DocumentLocaleKey documentLocaleKey = new DocumentLocaleKey(sampleHTextFlow.getDocument().getId(),
                    hLocale.getLocaleId());
            TextFlowTargetStateEvent tftUpdatedEvent = new TextFlowTargetStateEvent(documentLocaleKey,
                    projectIteration.getId(), authenticatedAccount.getPerson().getId(),
                    ImmutableList.copyOf(targetStates));
            textFlowTargetStateEvent.fire(tftUpdatedEvent);
            DocStatsEvent docEvent = new DocStatsEvent(documentLocaleKey, projectIteration.getId(),
                    contentStateDeltas, Iterables.getLast(targetStates).getTextFlowTargetId());
            docStatsEvent.fire(docEvent);
        }
        return results;
    }

    private void validateReviewPermissionIfApplicable(List<TransUnitUpdateRequest> translationRequests,
            HProjectIteration projectIteration, HLocale hLocale) {
        Optional<TransUnitUpdateRequest> hasReviewRequest = Iterables.tryFind(translationRequests,
                new Predicate<TransUnitUpdateRequest>() {

                    @Override
                    public boolean apply(TransUnitUpdateRequest input) {
                        return isReviewState(input.getNewContentState());
                    }
                });
        if (hasReviewRequest.isPresent()) {
            identity.checkPermission("translation-review", projectIteration.getProject(), hLocale);
        }
    }

    private static boolean isReviewState(ContentState contentState) {
        return contentState == ContentState.Approved || contentState == ContentState.Rejected;
    }

    /**
     * Generate a {@link HLocale} for the given localeId and check that
     * translations for this locale are permitted.
     *
     * @param localeId
     * @param projectIteration
     * @return the valid hLocale
     * @throws ZanataServiceException
     *             if the locale is not enabled for the project-iteration or
     *             server
     */
    private HLocale validateLocale(LocaleId localeId, HProjectIteration projectIteration)
            throws ZanataServiceException {
        String projectSlug = projectIteration.getProject().getSlug();
        return localeServiceImpl.validateLocaleByProjectIteration(localeId, projectSlug,
                projectIteration.getSlug());
    }

    public static class TranslationDetails {
        private final String revisionComment;
        private final EntityType copiedEntityType;
        private final TranslationSourceType sourceType;
        private final Long copiedEntityId;

        public TranslationDetails(String revisionComment, EntityType copiedEntityType,
                TranslationSourceType sourceType, Long copiedEntityId) {
            this.revisionComment = revisionComment;
            this.sourceType = sourceType;
            this.copiedEntityType = copiedEntityType;
            this.copiedEntityId = copiedEntityId;
        }

        public TranslationDetails(TransUnitUpdateRequest request) {
            this.revisionComment = request.getRevisionComment();
            this.copiedEntityId = request.getCopiedEntityId();
            copiedEntityType = StringUtils.isEmpty(request.getCopiedEntityType()) ? null
                    : EntityType.getValueOf(request.getCopiedEntityType());
            sourceType = StringUtils.isEmpty(request.getSourceType()) ? null
                    : TranslationSourceType.getValueOf(request.getSourceType());
        }

        public String getRevisionComment() {
            return revisionComment;
        }

        public EntityType getCopiedEntityType() {
            return copiedEntityType;
        }

        public TranslationSourceType getSourceType() {
            return sourceType;
        }

        public Long getCopiedEntityId() {
            return copiedEntityId;
        }
    }

    private boolean translate(@Nonnull HTextFlowTarget hTextFlowTarget, @Nonnull List<String> contentsToSave,
            ContentState requestedState, int nPlurals, TranslationDetails details) {
        boolean targetChanged = false;
        targetChanged |= setContentIfChanged(hTextFlowTarget, contentsToSave);
        targetChanged |= setContentStateIfChanged(requestedState, hTextFlowTarget, nPlurals);
        if (targetChanged || hTextFlowTarget.getVersionNum() == 0) {
            HTextFlow textFlow = hTextFlowTarget.getTextFlow();
            hTextFlowTarget.setVersionNum(hTextFlowTarget.getVersionNum() + 1);
            hTextFlowTarget.setTextFlowRevision(textFlow.getRevision());
            hTextFlowTarget.setLastModifiedBy(authenticatedAccount.getPerson());
            hTextFlowTarget.setRevisionComment(details.getRevisionComment());
            hTextFlowTarget.setCopiedEntityType(details.getCopiedEntityType());
            hTextFlowTarget.setSourceType(details.getSourceType());
            hTextFlowTarget.setCopiedEntityId(details.getCopiedEntityId());
            log.debug("last modified by :{}", authenticatedAccount.getPerson().getName());
        }
        // save the target histories
        entityManager.flush();
        return targetChanged;
    }

    /**
     * @return true if the content was changed, false otherwise
     */
    private boolean setContentIfChanged(@Nonnull HTextFlowTarget hTextFlowTarget,
            @Nonnull List<String> contentsToSave) {
        if (!contentsToSave.equals(hTextFlowTarget.getContents())) {
            hTextFlowTarget.setContents(contentsToSave);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Check that requestedState is valid for the given content, adjust if
     * necessary and set the new state if it has changed.
     *
     * @return true if the content state or contents list were updated, false
     *         otherwise
     * @see #adjustContentsAndState(org.zanata.model.HTextFlowTarget, int,
     *      java.util.List)
     */
    private boolean setContentStateIfChanged(@Nonnull ContentState requestedState, @Nonnull HTextFlowTarget target,
            int nPlurals) {
        boolean changed = false;
        ContentState previousState = target.getState();
        target.setState(requestedState);
        ArrayList<String> warnings = new ArrayList<String>();
        changed |= adjustContentsAndState(target, nPlurals, warnings);
        for (String warning : warnings) {
            log.warn(warning);
        }
        if (isReviewState(target.getState())) {
            // reviewer saved it
            target.setReviewer(authenticatedAccount.getPerson());
            if (previousState == ContentState.New) {
                target.setTranslator(authenticatedAccount.getPerson());
            }
        } else {
            target.setTranslator(authenticatedAccount.getPerson());
        }
        if (target.getState() != previousState) {
            changed = true;
        }
        return changed;
    }

    /**
     * Checks target state against its contents. If necessary, modifies target
     * state and generates a warning
     *
     * @param target
     *            HTextFlowTarget to check/modify
     * @param nPlurals
     *            number of plurals for this locale for this message: use 1 if
     *            message does not support plurals
     * @param warnings
     *            a warning string will be added if state is adjusted
     * @return true if and only if some state was changed
     */
    private static boolean adjustContentsAndState(@Nonnull HTextFlowTarget target, int nPlurals,
            @Nonnull List<String> warnings) {
        ContentState oldState = target.getState();
        String resId = target.getTextFlow().getResId();
        boolean contentsChanged = ensureContentsSize(target, nPlurals, resId, warnings);
        List<String> contents = target.getContents();
        target.setState(ContentStateUtil.determineState(oldState, contents, resId, warnings));
        boolean stateChanged = (oldState != target.getState());
        return contentsChanged || stateChanged;
    }

    /**
     * Ensures that target.contents has exactly legalSize elements
     *
     * @param target
     *            HTextFlowTarget to check/modify
     * @param legalSize
     *            required number of contents
     * @param resId
     *            ID of target
     * @param warnings
     *            if elements were added or removed
     * @return
     */
    private static boolean ensureContentsSize(HTextFlowTarget target, int legalSize, String resId,
            @Nonnull List<String> warnings) {
        int contentsSize = target.getContents().size();
        if (contentsSize < legalSize) {
            String warning = "Should have " + legalSize + " contents; adding empty strings: TextFlowTarget " + resId
                    + " with contents: " + target.getContents();
            warnings.add(warning);
            List<String> newContents = new ArrayList<String>(legalSize);
            newContents.addAll(target.getContents());
            while (newContents.size() < legalSize) {
                newContents.add("");
            }
            target.setContents(newContents);
            return true;
        } else if (contentsSize > legalSize) {
            String warning = "Should have " + legalSize + " contents; discarding extra strings: TextFlowTarget "
                    + resId + " with contents: " + target.getContents();
            warnings.add(warning);
            List<String> newContents = new ArrayList<String>(legalSize);
            for (int i = 0; i < legalSize; i++) {
                String content = target.getContents().get(i);
                newContents.add(content);
            }
            target.setContents(newContents);
            return true;
        }
        return false;
    }
    // This will not run in a transaction. Instead, transactions are controlled
    // within the method itself.

    @Override
    @Async
    public CompletableFuture<List<String>> translateAllInDocAsync(String projectSlug, String iterationSlug,
            String docId, LocaleId locale, TranslationsResource translations, Set<String> extensions,
            MergeType mergeType, boolean assignCreditToUploader, boolean lock, AsyncTaskHandle handle,
            TranslationSourceType translationSourceType) {
        // Lock this document for push
        Lock transLock = null;
        if (lock) {
            transLock = new Lock(projectSlug, iterationSlug, docId, locale, "push");
            lockManagerServiceImpl.attain(transLock);
        }
        List<String> warnings;
        try {
            warnings = this.translateAllInDoc(projectSlug, iterationSlug, docId, locale, translations, extensions,
                    mergeType, assignCreditToUploader, handle, translationSourceType);
        } finally {
            if (lock) {
                lockManagerServiceImpl.release(transLock);
            }
        }
        return AsyncTaskResult.completed(warnings);
    }

    /**
     * Run enforced validation check(Error) if target has changed and
     * translation saving as 'Translated' or 'Approved'
     *
     * @param newState
     * @param projectVersion
     * @param targetId
     * @param sources
     * @param translations
     * @return error messages
     */
    private String validateTranslations(ContentState newState, HProjectIteration projectVersion, String targetId,
            List<String> sources, List<String> translations) {
        String message = null;
        if (newState.isTranslated()) {
            List<String> validationMessages = validationServiceImpl.validateWithServerRules(projectVersion, sources,
                    translations, ValidationAction.State.Error);
            if (!validationMessages.isEmpty()) {
                StringBuilder sb = new StringBuilder();
                for (String validationMessage : validationMessages) {
                    sb.append(validationMessage).append("\n");
                }
                message = msgs.format("jsf.TranslationContainsError", ShortString.shorten(translations.get(0)),
                        sb.toString());
            }
        }
        return message;
    }

    @Override
    @Transactional
    public List<String> translateAllInDoc(final String projectSlug, final String iterationSlug, final String docId,
            final LocaleId locale, final TranslationsResource translations, final Set<String> extensions,
            final MergeType mergeType, final boolean assignCreditToUploader,
            final TranslationSourceType translationSourceType) {
        return translateAllInDoc(projectSlug, iterationSlug, docId, locale, translations, extensions, mergeType,
                assignCreditToUploader, null, translationSourceType);
    }
    // can't use @Transactional because we call runInTransaction

    @Override
    public List<String> translateAllInDoc(final String projectSlug, final String iterationSlug, final String docId,
            final LocaleId locale, final TranslationsResource translations, final Set<String> extensions,
            final MergeType mergeType, final boolean assignCreditToUploader, AsyncTaskHandle handle,
            final TranslationSourceType translationSourceType) {
        final HProjectIteration hProjectIteration = projectIterationDAO.getBySlug(projectSlug, iterationSlug);
        if (hProjectIteration == null) {
            throw new ZanataServiceException(
                    "Version \'" + iterationSlug + "\' for project \'" + projectSlug + "\' ");
        }
        if (mergeType == MergeType.IMPORT) {
            identity.checkPermission("import-translation", hProjectIteration);
        }
        ResourceUtils.validateExtensions(extensions);
        log.debug("pass evaluate");
        final HDocument document = documentDAO.getByDocIdAndIteration(hProjectIteration, docId);
        if (document == null || document.isObsolete()) {
            throw new ZanataServiceException("A document was not found.", 404);
        }
        log.debug("start put translations entity:{}", translations);
        boolean changed = false;
        final HLocale hLocale = localeServiceImpl.validateLocaleByProjectIteration(locale, projectSlug,
                iterationSlug);
        final Optional<AsyncTaskHandle> handleOp = Optional.fromNullable(handle);
        if (handleOp.isPresent()) {
            handleOp.get().setMaxProgress(translations.getTextFlowTargets().size());
        }
        try {
            changed |= runInTransaction(() ->
            // handle extensions
            resourceUtils.transferFromTranslationsResourceExtensions(translations.getExtensions(true), document,
                    extensions, hLocale, mergeType));
        } catch (Exception e) {
            log.error("exception in transferFromTranslationsResourceExtensions: {}", e.getMessage());
            throw new ZanataServiceException("Error during translation.", 500, e);
        }
        // NB: removedTargets only applies for MergeType.IMPORT
        final Collection<Long> removedTextFlowTargetIds = new HashSet<>();
        final List<String> warnings = new ArrayList<String>();
        if (mergeType == MergeType.IMPORT) {
            for (HTextFlow textFlow : document.getTextFlows()) {
                HTextFlowTarget hTarget = textFlow.getTargets().get(hLocale.getId());
                if (hTarget != null) {
                    removedTextFlowTargetIds.add(hTarget.getId());
                }
            }
        }
        // Break the target into batches
        List<List<TextFlowTarget>> batches = Lists.partition(translations.getTextFlowTargets(), BATCH_SIZE);
        for (final List<TextFlowTarget> batch : batches) {
            try {
                changed |= runInTransaction(() -> saveBatch(extensions, warnings, hLocale, document, mergeType,
                        removedTextFlowTargetIds, handleOp, hProjectIteration.getId(), batch,
                        assignCreditToUploader, translationSourceType));
            } catch (Exception e) {
                log.error("exception saving translation batch: {}", e.getMessage());
                throw new ZanataServiceException("Error during translation.", 500, e);
            }
        }
        if (changed || !removedTextFlowTargetIds.isEmpty()) {
            try {
                runInTransaction(() -> {
                    for (Long targetId : removedTextFlowTargetIds) {
                        // need to refresh from persistence
                        HTextFlowTarget target = textFlowTargetDAO.findById(targetId, true);
                        target.clear();
                    }
                    textFlowTargetDAO.flush();
                    documentDAO.flush();
                    return null;
                });
                Long actorId = authenticatedAccount.getPerson().getId();
                documentUploadedEvent
                        .fire(new DocumentUploadedEvent(actorId, document.getId(), false, hLocale.getLocaleId()));
            } catch (Exception e) {
                log.error("exception in removeTargets: {}", e.getMessage());
                throw new ZanataServiceException("Error during translation.", 500, e);
            }
        }
        return warnings;
    }

    private int getNumPlurals(HLocale hLocale, HTextFlow textFlow) {
        int nPlurals;
        if (!textFlow.isPlural()) {
            nPlurals = 1;
        } else {
            nPlurals = resourceUtils.getNumPlurals(textFlow.getDocument(), hLocale);
        }
        return nPlurals;
    }

    private Boolean saveBatch(final Set<String> extensions, final List<String> warnings, final HLocale locale,
            HDocument document, final MergeType mergeType, final Collection<Long> removedTextFlowTargetIds,
            final Optional<AsyncTaskHandle> handleOp, final Long projectIterationId,
            final List<TextFlowTarget> batch, final boolean assignCreditToUploader,
            final TranslationSourceType translationSourceType) {
        // we need to call clear at the beginning because text flow target
        // history rely on after commit callback.
        textFlowTargetDAO.clear();
        document = entityManager.find(HDocument.class, document.getId());
        boolean changed = false;
        // we need a fresh object in this session,
        // so that it can lazily load associated objects
        HProjectIteration iteration = projectIterationDAO.findById(projectIterationId);
        Map<String, HTextFlow> resIdToTextFlowMap = textFlowDAO.getByDocumentAndResIds(document,
                Lists.transform(batch, new Function<TextFlowTarget, String>() {

                    @Override
                    public String apply(TextFlowTarget input) {
                        return input.getResId();
                    }
                }));
        final int numPlurals = resourceUtils.getNumPlurals(document, locale);
        List<TextFlowTargetStateChange> targetStates = Lists.newArrayList();
        Map<ContentState, Long> contentStateDeltas = Maps.newHashMap();
        for (TextFlowTarget incomingTarget : batch) {
            String resId = incomingTarget.getResId();
            String sourceHash = incomingTarget.getSourceHash();
            HTextFlow textFlow = resIdToTextFlowMap.get(resId);
            if (textFlow == null) {
                // return warning for unknown resId to caller
                String warning = "Could not find TextFlow for TextFlowTarget " + resId + " with contents: "
                        + incomingTarget.getContents();
                warnings.add(warning);
                log.warn("skipping TextFlowTarget with unknown resId: {}", resId);
            } else if (sourceHash != null && !sourceHash.equals(textFlow.getContentHash())) {
                String warning = MessageFormat.format(
                        "TextFlowTarget {0} may be obsolete; associated source hash: {1}; expected hash is {2} for source: {3}",
                        resId, sourceHash, textFlow.getContentHash(), textFlow.getContents());
                warnings.add(warning);
                log.warn("skipping TextFlowTarget {} with unknown sourceHash: {}", resId, sourceHash);
            } else {
                String validationMessage = validateTranslations(incomingTarget.getState(), iteration,
                        incomingTarget.getResId(), textFlow.getContents(), incomingTarget.getContents());
                if (!StringUtils.isEmpty(validationMessage)) {
                    warnings.add(validationMessage);
                    log.warn(validationMessage);
                    continue;
                }
                int nPlurals = textFlow.isPlural() ? numPlurals : 1;
                // we have eagerly loaded all targets upfront
                HTextFlowTarget hTarget = textFlow.getTargets().get(locale.getId());
                ContentState oldState = ContentState.New;
                if (hTarget != null) {
                    oldState = hTarget.getState();
                    if (mergeType == MergeType.IMPORT) {
                        removedTextFlowTargetIds.remove(hTarget.getId());
                    }
                }
                TranslationMergeServiceFactory.MergeContext mergeContext = new TranslationMergeServiceFactory.MergeContext(
                        mergeType, textFlow, locale, hTarget, nPlurals);
                TranslationMergeService mergeService = translationMergeServiceFactory.getMergeService(mergeContext);
                boolean targetChanged = mergeService.merge(incomingTarget, hTarget, extensions);
                if (hTarget == null) {
                    // in case hTarget was null, we need to
                    // retrieve it after merge
                    hTarget = textFlow.getTargets().get(locale.getId());
                }
                targetChanged |= adjustContentsAndState(hTarget, nPlurals, warnings);
                // update translation information if applicable
                if (targetChanged) {
                    hTarget.setVersionNum(hTarget.getVersionNum() + 1);
                    changed = true;
                    if (assignCreditToUploader) {
                        HPerson hPerson = authenticatedAccount.getPerson();
                        hTarget.setTranslator(hPerson);
                        hTarget.setLastModifiedBy(hPerson);
                    } else {
                        hTarget.setTranslator(null);
                        hTarget.setLastModifiedBy(authenticatedAccount.getPerson());
                    }
                    hTarget.setSourceType(translationSourceType);
                    hTarget.setCopiedEntityId(null);
                    hTarget.setCopiedEntityId(null);
                    textFlowTargetDAO.makePersistent(hTarget);
                    aggregateChanges(textFlow, hTarget, oldState, targetStates, contentStateDeltas);
                }
            }
            if (handleOp.isPresent()) {
                handleOp.get().increaseProgress(1);
            }
        }
        if (!targetStates.isEmpty()) {
            Long actorId = assignCreditToUploader ? authenticatedAccount.getPerson().getId() : null;
            DocumentLocaleKey documentLocaleKey = new DocumentLocaleKey(document.getId(), locale.getLocaleId());
            TextFlowTargetStateEvent tftUpdatedEvent = new TextFlowTargetStateEvent(documentLocaleKey,
                    projectIterationId, actorId, ImmutableList.copyOf(targetStates));
            textFlowTargetStateEvent.fire(tftUpdatedEvent);
            DocStatsEvent docEvent = new DocStatsEvent(documentLocaleKey, projectIterationId, contentStateDeltas,
                    Iterables.getLast(tftUpdatedEvent.getStates()).getTextFlowTargetId());
            docStatsEvent.fire(docEvent);
        }
        textFlowTargetDAO.flush();
        return changed;
    }

    private void aggregateChanges(HTextFlow textFlow, HTextFlowTarget hTarget, ContentState oldState,
            List<TextFlowTargetStateChange> targetStates, Map<ContentState, Long> contentStateDeltas) {
        TextFlowTargetStateChange state = new TextFlowTargetStateChange(textFlow.getId(), hTarget.getId(),
                hTarget.getState(), oldState);
        targetStates.add(state);
        DocStatsEvent.updateContentStateDeltas(contentStateDeltas, state.getNewState(), state.getPreviousState(),
                textFlow.getWordCount());
    }

    public static class TranslationResultImpl implements TranslationResult {

        private HTextFlowTarget translatedTextFlowTarget;
        private boolean isSuccess;
        private boolean targetChanged = false;
        private boolean isVersionNumConflict = false;
        private int baseVersion;
        private ContentState baseContentState;
        private String errorMessage;

        @Override
        public boolean isTranslationSuccessful() {
            return isSuccess;
        }

        @Override
        public boolean isVersionNumConflict() {
            return isVersionNumConflict;
        }

        @Override
        public boolean isTargetChanged() {
            return targetChanged;
        }

        @Override
        public HTextFlowTarget getTranslatedTextFlowTarget() {
            return translatedTextFlowTarget;
        }

        @Override
        public int getBaseVersionNum() {
            return baseVersion;
        }

        @Override
        public ContentState getBaseContentState() {
            return baseContentState;
        }

        @Override
        public String getErrorMessage() {
            return errorMessage;
        }
    }

    @Override
    @Transactional
    public List<TranslationResult> revertTranslations(LocaleId localeId,
            List<TransUnitUpdateInfo> translationsToRevert) {
        List<TranslationResult> results = new ArrayList<TranslationResult>();
        List<TransUnitUpdateRequest> updateRequests = new ArrayList<TransUnitUpdateRequest>();
        if (!translationsToRevert.isEmpty()) {
            HTextFlow sampleHTextFlow = entityManager.find(HTextFlow.class,
                    translationsToRevert.get(0).getTransUnit().getId().getValue());
            HLocale hLocale = validateLocale(localeId, sampleHTextFlow.getDocument().getProjectIteration());
            for (TransUnitUpdateInfo info : translationsToRevert) {
                if (!info.isSuccess() || !info.isTargetChanged()) {
                    continue;
                }
                TransUnitId tuId = info.getTransUnit().getId();
                HTextFlow hTextFlow = entityManager.find(HTextFlow.class, tuId.getValue());
                HTextFlowTarget hTextFlowTarget = textFlowTargetDAO.getOrCreateTarget(hTextFlow, hLocale);
                // check that version has not advanced
                // TODO probably also want to check that source has not been
                // updated
                Integer versionNum = hTextFlowTarget.getVersionNum();
                log.debug("about to revert hTextFlowTarget version {} to TransUnit version {}", versionNum,
                        info.getTransUnit().getVerNum());
                if (versionNum.equals(info.getTransUnit().getVerNum())) {
                    // look up replaced version
                    HTextFlowTargetHistory oldTarget = hTextFlowTarget.getHistory()
                            .get(info.getPreviousVersionNum());
                    if (oldTarget != null) {
                        // generate request
                        List<String> oldContents = oldTarget.getContents();
                        ContentState oldState = oldTarget.getState();
                        String copiedEntityType = oldTarget.getCopiedEntityType() == null ? null
                                : oldTarget.getCopiedEntityType().getAbbr();
                        String sourceType = oldTarget.getSourceType() == null
                                ? TranslationSourceType.UNKNOWN.getAbbr()
                                : oldTarget.getSourceType().getAbbr();
                        TransUnitUpdateRequest request = new TransUnitUpdateRequest(tuId, oldContents, oldState,
                                versionNum, oldTarget.getRevisionComment(), oldTarget.getCopiedEntityId(),
                                copiedEntityType, sourceType);
                        // add to list
                        updateRequests.add(request);
                    } else {
                        log.info(
                                "got null previous target for tu with id {}, version {}. Assuming previous state is untranslated",
                                hTextFlow.getId(), info.getPreviousVersionNum());
                        List<String> emptyContents = Lists.newArrayList();
                        for (int i = 0; i < hTextFlowTarget.getContents().size(); i++) {
                            emptyContents.add("");
                        }
                        TransUnitUpdateRequest request = new TransUnitUpdateRequest(tuId, emptyContents,
                                ContentState.New, versionNum, TranslationSourceType.UNKNOWN.getAbbr());
                        updateRequests.add(request);
                    }
                } else {
                    log.info(
                            "attempt to revert target version {} for tu with id {}, but current version is {}. Not reverting.",
                            new Object[] { info.getTransUnit().getVerNum(), tuId, versionNum });
                    results.add(buildFailResult(hTextFlowTarget));
                }
            }
        }
        results.addAll(translateWithoutValidating(localeId, updateRequests));
        return results;
    }

    /**
     * @param hTextFlowTarget
     * @return
     */
    private TranslationResultImpl buildFailResult(HTextFlowTarget hTextFlowTarget) {
        TranslationResultImpl result = new TranslationResultImpl();
        result.baseVersion = hTextFlowTarget.getVersionNum();
        result.baseContentState = hTextFlowTarget.getState();
        result.isSuccess = false;
        result.isVersionNumConflict = false;
        result.translatedTextFlowTarget = hTextFlowTarget;
        result.errorMessage = null;
        return result;
    }
}