001/*
002 * (C) Copyright 2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Kevin Leturc <kleturc@nuxeo.com>
018 */
019package org.nuxeo.ecm.core.trash;
020
021import static java.lang.Boolean.TRUE;
022import static java.util.function.Predicate.isEqual;
023import static java.util.stream.Collectors.toList;
024import static org.nuxeo.ecm.core.api.LifeCycleConstants.UNDELETE_TRANSITION;
025import static org.nuxeo.ecm.core.query.sql.NXQL.ECM_UUID;
026import static org.nuxeo.ecm.core.trash.PropertyTrashService.SYSPROP_IS_TRASHED;
027import static org.nuxeo.ecm.core.trash.TrashServiceImpl.MIGRATION_STATE_LIFECYCLE;
028import static org.nuxeo.ecm.core.trash.TrashServiceImpl.MIGRATION_STATE_PROPERTY;
029import static org.nuxeo.ecm.core.trash.TrashServiceImpl.MIGRATION_STEP_LIFECYCLE_TO_PROPERTY;
030
031import java.io.Serializable;
032import java.util.List;
033import java.util.Map;
034
035import org.nuxeo.ecm.core.api.AbstractSession;
036import org.nuxeo.ecm.core.api.CoreSession;
037import org.nuxeo.ecm.core.api.LifeCycleException;
038import org.nuxeo.ecm.core.api.NuxeoException;
039import org.nuxeo.ecm.core.lifecycle.LifeCycleService;
040import org.nuxeo.ecm.core.migrator.AbstractRepositoryMigrator;
041import org.nuxeo.ecm.core.model.Document;
042import org.nuxeo.ecm.core.repository.RepositoryService;
043import org.nuxeo.runtime.api.Framework;
044import org.nuxeo.runtime.migration.MigrationService.MigrationContext;
045import org.nuxeo.runtime.transaction.TransactionHelper;
046
047/**
048 * Migrator of trashed state.
049 *
050 * @since 10.2
051 */
052public class TrashedStateMigrator extends AbstractRepositoryMigrator {
053
054    protected static final String QUERY_DELETED = "SELECT ecm:uuid FROM Document WHERE ecm:currentLifeCycleState = 'deleted' AND ecm:isVersion = 0";
055
056    protected static final int BATCH_SIZE = 50;
057
058    @Override
059    public void notifyStatusChange() {
060        TrashServiceImpl trashService = Framework.getService(TrashServiceImpl.class);
061        trashService.invalidateTrashServiceImplementation();
062    }
063
064    @Override
065    public String probeState() {
066        List<String> repositoryNames = Framework.getService(RepositoryService.class).getRepositoryNames();
067        if (repositoryNames.stream().map(this::probeRepository).anyMatch(isEqual(MIGRATION_STATE_LIFECYCLE))) {
068            return MIGRATION_STATE_LIFECYCLE;
069        }
070        return MIGRATION_STATE_PROPERTY;
071    }
072
073    @Override
074    protected String probeSession(CoreSession session) {
075        // finds if there are documents in 'deleted' lifecycle state
076        List<Map<String, Serializable>> deletedMaps = session.queryProjection(QUERY_DELETED, 1, 0); // limit 1
077        if (!deletedMaps.isEmpty()) {
078            return MIGRATION_STATE_LIFECYCLE;
079        } else {
080            return MIGRATION_STATE_PROPERTY;
081        }
082    }
083
084    @Override
085    public void run(String step, MigrationContext migrationContext) {
086        if (!MIGRATION_STEP_LIFECYCLE_TO_PROPERTY.equals(step)) {
087            throw new NuxeoException("Unknown migration step: " + step);
088        }
089        this.migrationContext = migrationContext;
090        reportProgress("Initializing", 0, -1); // unknown
091        List<String> repositoryNames = Framework.getService(RepositoryService.class).getRepositoryNames();
092        try {
093            repositoryNames.forEach(repoName -> migrateRepository(step, migrationContext, repoName));
094            reportProgress("Done", 1, 1);
095        } catch (MigrationShutdownException e) {
096            return;
097        }
098    }
099
100    @Override
101    protected void migrateSession(String step, MigrationContext migrationContext, CoreSession session) {
102        // query all 'deleted' documents
103        List<Map<String, Serializable>> deletedMaps = session.queryProjection(QUERY_DELETED, -1, 0);
104
105        checkShutdownRequested(migrationContext);
106
107        // compute all deleted doc id refs
108        List<String> ids = deletedMaps.stream().map(map -> (String) map.get(ECM_UUID)).collect(toList());
109
110        checkShutdownRequested(migrationContext);
111
112        // set ecm:isTrashed property by batch
113        int size = ids.size();
114        int i = 0;
115        reportProgress(session.getRepositoryName(), "Setting isTrashed property", i, size);
116        LifeCycleService lifeCycleService = Framework.getService(LifeCycleService.class);
117        for (String id : ids) {
118            // here we need the low level Document in order to workaround the backward mechanism of followTransition
119            // present in the CoreSession
120            Document doc = ((AbstractSession) session).getSession().getDocumentByUUID(id);
121            // set trash property to true
122            doc.setSystemProp(SYSPROP_IS_TRASHED, TRUE);
123            // try to follow undelete transition
124            try {
125                lifeCycleService.followTransition(doc, UNDELETE_TRANSITION);
126            } catch (LifeCycleException e) {
127                // this is possible if the lifecycle policy doesn't exist anymore
128                // force 'project' state
129                doc.setCurrentLifeCycleState("project");
130            }
131            checkShutdownRequested(migrationContext);
132            i++;
133            if (i % BATCH_SIZE == 0 || i == size) {
134                reportProgress(session.getRepositoryName(), "Setting isTrashed property", i, size);
135                TransactionHelper.commitOrRollbackTransaction();
136                TransactionHelper.startTransaction();
137            }
138        }
139        reportProgress(session.getRepositoryName(), "Done", size, size);
140    }
141}