001/*
002 * (C) Copyright 2006-2015 Nuxeo SA (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 *     Bogdan Stefanescu
018 *     Florent Guillaume
019 *     Thierry Delprat
020 */
021package org.nuxeo.ecm.core;
022
023import java.io.Serializable;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedHashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033import java.util.concurrent.atomic.AtomicLong;
034
035import org.nuxeo.ecm.core.api.CoreInstance;
036import org.nuxeo.ecm.core.api.CoreSession;
037import org.nuxeo.ecm.core.api.IdRef;
038import org.nuxeo.ecm.core.api.IterableQueryResult;
039import org.nuxeo.ecm.core.query.sql.NXQL;
040import org.nuxeo.ecm.core.repository.RepositoryService;
041import org.nuxeo.ecm.core.versioning.DefaultVersionRemovalPolicy;
042import org.nuxeo.ecm.core.versioning.OrphanVersionRemovalFilter;
043import org.nuxeo.ecm.core.versioning.VersionRemovalPolicy;
044import org.nuxeo.runtime.api.Framework;
045import org.nuxeo.runtime.model.ComponentContext;
046import org.nuxeo.runtime.model.ComponentInstance;
047import org.nuxeo.runtime.model.DefaultComponent;
048import org.nuxeo.runtime.transaction.TransactionHelper;
049
050/**
051 * Service used to register version removal policies.
052 */
053public class CoreService extends DefaultComponent {
054
055    private static final String VERSION_REMOVAL_POLICY_XP = "versionRemovalPolicy";
056
057    private static final String ORPHAN_VERSION_REMOVAL_FILTER_XP = "orphanVersionRemovalFilter";
058
059    protected static final DefaultVersionRemovalPolicy DEFAULT_VERSION_REMOVAL_POLICY = new DefaultVersionRemovalPolicy();
060
061    protected Map<CoreServicePolicyDescriptor, VersionRemovalPolicy> versionRemovalPolicies = new LinkedHashMap<>();
062
063    protected Map<CoreServiceOrphanVersionRemovalFilterDescriptor, OrphanVersionRemovalFilter> orphanVersionRemovalFilters = new LinkedHashMap<>();
064
065    protected ComponentContext context;
066
067    @Override
068    public void activate(ComponentContext context) {
069        this.context = context;
070    }
071
072    @Override
073    public void deactivate(ComponentContext context) {
074        this.context = null;
075    }
076
077    @Override
078    public void registerContribution(Object contrib, String point, ComponentInstance contributor) {
079        if (VERSION_REMOVAL_POLICY_XP.equals(point)) {
080            registerVersionRemovalPolicy((CoreServicePolicyDescriptor) contrib);
081        } else if (ORPHAN_VERSION_REMOVAL_FILTER_XP.equals(point)) {
082            registerOrphanVersionRemovalFilter((CoreServiceOrphanVersionRemovalFilterDescriptor) contrib);
083        } else {
084            throw new RuntimeException("Unknown extension point: " + point);
085        }
086    }
087
088    @Override
089    public void unregisterContribution(Object contrib, String point, ComponentInstance contributor) {
090        if (VERSION_REMOVAL_POLICY_XP.equals(point)) {
091            unregisterVersionRemovalPolicy((CoreServicePolicyDescriptor) contrib);
092        } else if (ORPHAN_VERSION_REMOVAL_FILTER_XP.equals(point)) {
093            unregisterOrphanVersionRemovalFilter((CoreServiceOrphanVersionRemovalFilterDescriptor) contrib);
094        }
095    }
096
097    protected void registerVersionRemovalPolicy(CoreServicePolicyDescriptor contrib) {
098        String klass = contrib.getKlass();
099        try {
100            VersionRemovalPolicy policy = (VersionRemovalPolicy) context.getRuntimeContext().loadClass(klass).newInstance();
101            versionRemovalPolicies.put(contrib, policy);
102        } catch (ReflectiveOperationException e) {
103            throw new RuntimeException("Failed to instantiate " + VERSION_REMOVAL_POLICY_XP + ": " + klass, e);
104        }
105    }
106
107    protected void unregisterVersionRemovalPolicy(CoreServicePolicyDescriptor contrib) {
108        versionRemovalPolicies.remove(contrib);
109    }
110
111    protected void registerOrphanVersionRemovalFilter(CoreServiceOrphanVersionRemovalFilterDescriptor contrib) {
112        String klass = contrib.getKlass();
113        try {
114            OrphanVersionRemovalFilter filter = (OrphanVersionRemovalFilter) context.getRuntimeContext().loadClass(
115                    klass).newInstance();
116            orphanVersionRemovalFilters.put(contrib, filter);
117        } catch (ReflectiveOperationException e) {
118            throw new RuntimeException("Failed to instantiate " + ORPHAN_VERSION_REMOVAL_FILTER_XP + ": " + klass, e);
119        }
120    }
121
122    protected void unregisterOrphanVersionRemovalFilter(CoreServiceOrphanVersionRemovalFilterDescriptor contrib) {
123        orphanVersionRemovalFilters.remove(contrib);
124    }
125
126    /** Gets the last version removal policy registered. */
127    public VersionRemovalPolicy getVersionRemovalPolicy() {
128        if (versionRemovalPolicies.isEmpty()) {
129            return DEFAULT_VERSION_REMOVAL_POLICY;
130        } else {
131            VersionRemovalPolicy versionRemovalPolicy = null;
132            for (VersionRemovalPolicy policy : versionRemovalPolicies.values()) {
133                versionRemovalPolicy = policy;
134            }
135            return versionRemovalPolicy;
136        }
137    }
138
139    /** Gets all the orphan version removal filters registered. */
140    public Collection<OrphanVersionRemovalFilter> getOrphanVersionRemovalFilters() {
141        return orphanVersionRemovalFilters.values();
142    }
143
144    /**
145     * Removes the orphan versions.
146     * <p>
147     * A version stays referenced, and therefore is not removed, if any proxy points to a version in the version history
148     * of any live document, or in the case of tree snapshot if there is a snapshot containing a version in the version
149     * history of any live document.
150     *
151     * @param commitSize the maximum number of orphan versions to delete in one transaction
152     * @return the number of orphan versions deleted
153     * @since 9.1
154     */
155    public long cleanupOrphanVersions(long commitSize) {
156        RepositoryService repositoryService = Framework.getService(RepositoryService.class);
157        if (repositoryService == null) {
158            // not initialized
159            return 0;
160        }
161        List<String> repositoryNames = repositoryService.getRepositoryNames();
162        AtomicLong count = new AtomicLong();
163        for (String repositoryName : repositoryNames) {
164            TransactionHelper.runInTransaction(() -> {
165                CoreInstance.doPrivileged(repositoryName, (CoreSession session) -> {
166                    count.addAndGet(doCleanupOrphanVersions(session, commitSize));
167                });
168            });
169        }
170        return count.get();
171    }
172
173    protected long doCleanupOrphanVersions(CoreSession session, long commitSize) {
174        // compute map of version series -> list of version ids in it
175        Map<String, List<String>> versionSeriesToVersionIds = new HashMap<>();
176        String findVersions = "SELECT " + NXQL.ECM_UUID + ", " + NXQL.ECM_VERSION_VERSIONABLEID
177                + " FROM Document WHERE " + NXQL.ECM_ISVERSION + " = 1";
178        try (IterableQueryResult res = session.queryAndFetch(findVersions, NXQL.NXQL)) {
179            for (Map<String, Serializable> map : res) {
180                String versionSeriesId = (String) map.get(NXQL.ECM_VERSION_VERSIONABLEID);
181                String versionId = (String) map.get(NXQL.ECM_UUID);
182                versionSeriesToVersionIds.computeIfAbsent(versionSeriesId, k -> new ArrayList<>(4)).add(versionId);
183            }
184        }
185        Set<String> seriesIds = new HashSet<>();
186        // find the live doc ids
187        String findLive = "SELECT " + NXQL.ECM_UUID + " FROM Document WHERE " + NXQL.ECM_ISPROXY + " = 0 AND "
188                + NXQL.ECM_ISVERSION + " = 0";
189        try (IterableQueryResult res = session.queryAndFetch(findLive, NXQL.NXQL)) {
190            for (Map<String, Serializable> map : res) {
191                String id = (String) map.get(NXQL.ECM_UUID);
192                seriesIds.add(id);
193            }
194        }
195        // find the version series for proxies
196        String findProxies = "SELECT " + NXQL.ECM_PROXY_VERSIONABLEID + " FROM Document WHERE " + NXQL.ECM_ISPROXY
197                + " = 1";
198        try (IterableQueryResult res = session.queryAndFetch(findProxies, NXQL.NXQL)) {
199            for (Map<String, Serializable> map : res) {
200                String versionSeriesId = (String) map.get(NXQL.ECM_PROXY_VERSIONABLEID);
201                seriesIds.add(versionSeriesId);
202            }
203        }
204        // all version for series ids not found from live docs or proxies can be removed
205        Set<String> ids = new HashSet<>();
206        for (Entry<String, List<String>> en : versionSeriesToVersionIds.entrySet()) {
207            if (seriesIds.contains(en.getKey())) {
208                continue;
209            }
210            // not referenced -> remove
211            List<String> versionIds = en.getValue();
212            ids.addAll(versionIds);
213        }
214        // new transaction as we may have spent some time in the previous queries
215        TransactionHelper.commitOrRollbackTransaction();
216        TransactionHelper.startTransaction();
217        // remove these ids
218        if (!ids.isEmpty()) {
219            long n = 0;
220            for (String id : ids) {
221                session.removeDocument(new IdRef(id));
222                n++;
223                if (n >= commitSize) {
224                    session.save();
225                    TransactionHelper.commitOrRollbackTransaction();
226                    TransactionHelper.startTransaction();
227                    n = 0;
228                }
229            }
230            session.save();
231        }
232        return ids.size();
233    }
234
235}