/* * (C) Copyright 2006-2015 Nuxeo SA (http://nuxeo.com/) and others. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Contributors: * Bogdan Stefanescu * Florent Guillaume * Thierry Delprat */ package org.nuxeo.ecm.core; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import org.nuxeo.ecm.core.api.CoreInstance; import org.nuxeo.ecm.core.api.CoreSession; import org.nuxeo.ecm.core.api.IdRef; import org.nuxeo.ecm.core.api.IterableQueryResult; import org.nuxeo.ecm.core.query.sql.NXQL; import org.nuxeo.ecm.core.repository.RepositoryService; import org.nuxeo.ecm.core.versioning.DefaultVersionRemovalPolicy; import org.nuxeo.ecm.core.versioning.OrphanVersionRemovalFilter; import org.nuxeo.ecm.core.versioning.VersionRemovalPolicy; import org.nuxeo.runtime.api.Framework; import org.nuxeo.runtime.model.ComponentContext; import org.nuxeo.runtime.model.ComponentInstance; import org.nuxeo.runtime.model.DefaultComponent; import org.nuxeo.runtime.transaction.TransactionHelper; /** * Service used to register version removal policies. */ public class CoreService extends DefaultComponent { private static final String VERSION_REMOVAL_POLICY_XP = "versionRemovalPolicy"; private static final String ORPHAN_VERSION_REMOVAL_FILTER_XP = "orphanVersionRemovalFilter"; protected static final DefaultVersionRemovalPolicy DEFAULT_VERSION_REMOVAL_POLICY = new DefaultVersionRemovalPolicy(); protected Map<CoreServicePolicyDescriptor, VersionRemovalPolicy> versionRemovalPolicies = new LinkedHashMap<>(); protected Map<CoreServiceOrphanVersionRemovalFilterDescriptor, OrphanVersionRemovalFilter> orphanVersionRemovalFilters = new LinkedHashMap<>(); protected ComponentContext context; @Override public void activate(ComponentContext context) { this.context = context; } @Override public void deactivate(ComponentContext context) { this.context = null; } @Override public void registerContribution(Object contrib, String point, ComponentInstance contributor) { if (VERSION_REMOVAL_POLICY_XP.equals(point)) { registerVersionRemovalPolicy((CoreServicePolicyDescriptor) contrib); } else if (ORPHAN_VERSION_REMOVAL_FILTER_XP.equals(point)) { registerOrphanVersionRemovalFilter((CoreServiceOrphanVersionRemovalFilterDescriptor) contrib); } else { throw new RuntimeException("Unknown extension point: " + point); } } @Override public void unregisterContribution(Object contrib, String point, ComponentInstance contributor) { if (VERSION_REMOVAL_POLICY_XP.equals(point)) { unregisterVersionRemovalPolicy((CoreServicePolicyDescriptor) contrib); } else if (ORPHAN_VERSION_REMOVAL_FILTER_XP.equals(point)) { unregisterOrphanVersionRemovalFilter((CoreServiceOrphanVersionRemovalFilterDescriptor) contrib); } } protected void registerVersionRemovalPolicy(CoreServicePolicyDescriptor contrib) { String klass = contrib.getKlass(); try { VersionRemovalPolicy policy = (VersionRemovalPolicy) context.getRuntimeContext().loadClass(klass).newInstance(); versionRemovalPolicies.put(contrib, policy); } catch (ReflectiveOperationException e) { throw new RuntimeException("Failed to instantiate " + VERSION_REMOVAL_POLICY_XP + ": " + klass, e); } } protected void unregisterVersionRemovalPolicy(CoreServicePolicyDescriptor contrib) { versionRemovalPolicies.remove(contrib); } protected void registerOrphanVersionRemovalFilter(CoreServiceOrphanVersionRemovalFilterDescriptor contrib) { String klass = contrib.getKlass(); try { OrphanVersionRemovalFilter filter = (OrphanVersionRemovalFilter) context.getRuntimeContext().loadClass( klass).newInstance(); orphanVersionRemovalFilters.put(contrib, filter); } catch (ReflectiveOperationException e) { throw new RuntimeException("Failed to instantiate " + ORPHAN_VERSION_REMOVAL_FILTER_XP + ": " + klass, e); } } protected void unregisterOrphanVersionRemovalFilter(CoreServiceOrphanVersionRemovalFilterDescriptor contrib) { orphanVersionRemovalFilters.remove(contrib); } /** Gets the last version removal policy registered. */ public VersionRemovalPolicy getVersionRemovalPolicy() { if (versionRemovalPolicies.isEmpty()) { return DEFAULT_VERSION_REMOVAL_POLICY; } else { VersionRemovalPolicy versionRemovalPolicy = null; for (VersionRemovalPolicy policy : versionRemovalPolicies.values()) { versionRemovalPolicy = policy; } return versionRemovalPolicy; } } /** Gets all the orphan version removal filters registered. */ public Collection<OrphanVersionRemovalFilter> getOrphanVersionRemovalFilters() { return orphanVersionRemovalFilters.values(); } /** * Removes the orphan versions. * <p> * A version stays referenced, and therefore is not removed, if any proxy points to a version in the version history * of any live document, or in the case of tree snapshot if there is a snapshot containing a version in the version * history of any live document. * * @param commitSize the maximum number of orphan versions to delete in one transaction * @return the number of orphan versions deleted * @since 9.1 */ public long cleanupOrphanVersions(long commitSize) { RepositoryService repositoryService = Framework.getService(RepositoryService.class); if (repositoryService == null) { // not initialized return 0; } List<String> repositoryNames = repositoryService.getRepositoryNames(); AtomicLong count = new AtomicLong(); for (String repositoryName : repositoryNames) { TransactionHelper.runInTransaction(() -> { CoreInstance.doPrivileged(repositoryName, (CoreSession session) -> { count.addAndGet(doCleanupOrphanVersions(session, commitSize)); }); }); } return count.get(); } protected long doCleanupOrphanVersions(CoreSession session, long commitSize) { // compute map of version series -> list of version ids in it Map<String, List<String>> versionSeriesToVersionIds = new HashMap<>(); String findVersions = "SELECT " + NXQL.ECM_UUID + ", " + NXQL.ECM_VERSION_VERSIONABLEID + " FROM Document WHERE " + NXQL.ECM_ISVERSION + " = 1"; try (IterableQueryResult res = session.queryAndFetch(findVersions, NXQL.NXQL)) { for (Map<String, Serializable> map : res) { String versionSeriesId = (String) map.get(NXQL.ECM_VERSION_VERSIONABLEID); String versionId = (String) map.get(NXQL.ECM_UUID); versionSeriesToVersionIds.computeIfAbsent(versionSeriesId, k -> new ArrayList<>(4)).add(versionId); } } Set<String> seriesIds = new HashSet<>(); // find the live doc ids String findLive = "SELECT " + NXQL.ECM_UUID + " FROM Document WHERE " + NXQL.ECM_ISPROXY + " = 0 AND " + NXQL.ECM_ISVERSION + " = 0"; try (IterableQueryResult res = session.queryAndFetch(findLive, NXQL.NXQL)) { for (Map<String, Serializable> map : res) { String id = (String) map.get(NXQL.ECM_UUID); seriesIds.add(id); } } // find the version series for proxies String findProxies = "SELECT " + NXQL.ECM_PROXY_VERSIONABLEID + " FROM Document WHERE " + NXQL.ECM_ISPROXY + " = 1"; try (IterableQueryResult res = session.queryAndFetch(findProxies, NXQL.NXQL)) { for (Map<String, Serializable> map : res) { String versionSeriesId = (String) map.get(NXQL.ECM_PROXY_VERSIONABLEID); seriesIds.add(versionSeriesId); } } // all version for series ids not found from live docs or proxies can be removed Set<String> ids = new HashSet<>(); for (Entry<String, List<String>> en : versionSeriesToVersionIds.entrySet()) { if (seriesIds.contains(en.getKey())) { continue; } // not referenced -> remove List<String> versionIds = en.getValue(); ids.addAll(versionIds); } // new transaction as we may have spent some time in the previous queries TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); // remove these ids if (!ids.isEmpty()) { long n = 0; for (String id : ids) { session.removeDocument(new IdRef(id)); n++; if (n >= commitSize) { session.save(); TransactionHelper.commitOrRollbackTransaction(); TransactionHelper.startTransaction(); n = 0; } } session.save(); } return ids.size(); } }