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()
101                                                                        .loadClass(klass)
102                                                                        .getDeclaredConstructor()
103                                                                        .newInstance();
104            versionRemovalPolicies.put(contrib, policy);
105        } catch (ReflectiveOperationException e) {
106            throw new RuntimeException("Failed to instantiate " + VERSION_REMOVAL_POLICY_XP + ": " + klass, e);
107        }
108    }
109
110    protected void unregisterVersionRemovalPolicy(CoreServicePolicyDescriptor contrib) {
111        versionRemovalPolicies.remove(contrib);
112    }
113
114    protected void registerOrphanVersionRemovalFilter(CoreServiceOrphanVersionRemovalFilterDescriptor contrib) {
115        String klass = contrib.getKlass();
116        try {
117            OrphanVersionRemovalFilter filter = (OrphanVersionRemovalFilter) context.getRuntimeContext()
118                                                                                    .loadClass(klass)
119                                                                                    .getDeclaredConstructor()
120                                                                                    .newInstance();
121            orphanVersionRemovalFilters.put(contrib, filter);
122        } catch (ReflectiveOperationException e) {
123            throw new RuntimeException("Failed to instantiate " + ORPHAN_VERSION_REMOVAL_FILTER_XP + ": " + klass, e);
124        }
125    }
126
127    protected void unregisterOrphanVersionRemovalFilter(CoreServiceOrphanVersionRemovalFilterDescriptor contrib) {
128        orphanVersionRemovalFilters.remove(contrib);
129    }
130
131    /** Gets the last version removal policy registered. */
132    public VersionRemovalPolicy getVersionRemovalPolicy() {
133        if (versionRemovalPolicies.isEmpty()) {
134            return DEFAULT_VERSION_REMOVAL_POLICY;
135        } else {
136            VersionRemovalPolicy versionRemovalPolicy = null;
137            for (VersionRemovalPolicy policy : versionRemovalPolicies.values()) {
138                versionRemovalPolicy = policy;
139            }
140            return versionRemovalPolicy;
141        }
142    }
143
144    /** Gets all the orphan version removal filters registered. */
145    public Collection<OrphanVersionRemovalFilter> getOrphanVersionRemovalFilters() {
146        return orphanVersionRemovalFilters.values();
147    }
148
149    /**
150     * Removes the orphan versions.
151     * <p>
152     * A version stays referenced, and therefore is not removed, if any proxy points to a version in the version history
153     * of any live document, or in the case of tree snapshot if there is a snapshot containing a version in the version
154     * history of any live document.
155     *
156     * @param commitSize the maximum number of orphan versions to delete in one transaction
157     * @return the number of orphan versions deleted
158     * @since 9.1
159     */
160    public long cleanupOrphanVersions(long commitSize) {
161        RepositoryService repositoryService = Framework.getService(RepositoryService.class);
162        if (repositoryService == null) {
163            // not initialized
164            return 0;
165        }
166        List<String> repositoryNames = repositoryService.getRepositoryNames();
167        AtomicLong count = new AtomicLong();
168        for (String repositoryName : repositoryNames) {
169            TransactionHelper.runInTransaction(() -> {
170                CoreInstance.doPrivileged(repositoryName, (CoreSession session) -> {
171                    count.addAndGet(doCleanupOrphanVersions(session, commitSize));
172                });
173            });
174        }
175        return count.get();
176    }
177
178    protected long doCleanupOrphanVersions(CoreSession session, long commitSize) {
179        // compute map of version series -> list of version ids in it
180        Map<String, List<String>> versionSeriesToVersionIds = new HashMap<>();
181        String findVersions = "SELECT " + NXQL.ECM_UUID + ", " + NXQL.ECM_VERSION_VERSIONABLEID
182                + " FROM Document WHERE " + NXQL.ECM_ISVERSION + " = 1";
183        try (IterableQueryResult res = session.queryAndFetch(findVersions, NXQL.NXQL)) {
184            for (Map<String, Serializable> map : res) {
185                String versionSeriesId = (String) map.get(NXQL.ECM_VERSION_VERSIONABLEID);
186                String versionId = (String) map.get(NXQL.ECM_UUID);
187                versionSeriesToVersionIds.computeIfAbsent(versionSeriesId, k -> new ArrayList<>(4)).add(versionId);
188            }
189        }
190        Set<String> seriesIds = new HashSet<>();
191        // find the live doc ids
192        String findLive = "SELECT " + NXQL.ECM_UUID + " FROM Document WHERE " + NXQL.ECM_ISPROXY + " = 0 AND "
193                + NXQL.ECM_ISVERSION + " = 0";
194        try (IterableQueryResult res = session.queryAndFetch(findLive, NXQL.NXQL)) {
195            for (Map<String, Serializable> map : res) {
196                String id = (String) map.get(NXQL.ECM_UUID);
197                seriesIds.add(id);
198            }
199        }
200        // find the version series for proxies
201        String findProxies = "SELECT " + NXQL.ECM_PROXY_VERSIONABLEID + " FROM Document WHERE " + NXQL.ECM_ISPROXY
202                + " = 1";
203        try (IterableQueryResult res = session.queryAndFetch(findProxies, NXQL.NXQL)) {
204            for (Map<String, Serializable> map : res) {
205                String versionSeriesId = (String) map.get(NXQL.ECM_PROXY_VERSIONABLEID);
206                seriesIds.add(versionSeriesId);
207            }
208        }
209        // all version for series ids not found from live docs or proxies can be removed
210        Set<String> ids = new HashSet<>();
211        for (Entry<String, List<String>> en : versionSeriesToVersionIds.entrySet()) {
212            if (seriesIds.contains(en.getKey())) {
213                continue;
214            }
215            // not referenced -> remove
216            List<String> versionIds = en.getValue();
217            ids.addAll(versionIds);
218        }
219        // new transaction as we may have spent some time in the previous queries
220        TransactionHelper.commitOrRollbackTransaction();
221        TransactionHelper.startTransaction();
222        // remove these ids
223        if (!ids.isEmpty()) {
224            long n = 0;
225            for (String id : ids) {
226                session.removeDocument(new IdRef(id));
227                n++;
228                if (n >= commitSize) {
229                    session.save();
230                    TransactionHelper.commitOrRollbackTransaction();
231                    TransactionHelper.startTransaction();
232                    n = 0;
233                }
234            }
235            session.save();
236        }
237        return ids.size();
238    }
239
240}