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}