001/* 002 * Copyright 2021 Anyware Services 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 */ 016package org.ametys.workspaces.repository.maintenance; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022 023import javax.jcr.Credentials; 024import javax.jcr.NodeIterator; 025import javax.jcr.RepositoryException; 026import javax.jcr.Session; 027import javax.jcr.SimpleCredentials; 028import javax.jcr.query.Query; 029import javax.jcr.version.Version; 030import javax.jcr.version.VersionHistory; 031import javax.jcr.version.VersionIterator; 032 033import org.apache.jackrabbit.core.NodeImpl; 034import org.apache.jackrabbit.core.RepositoryImpl; 035import org.slf4j.LoggerFactory; 036 037/** 038 * Remove unused object from jcr history 039 */ 040public class RemoveUnusedHistoryTask extends AbstractMaintenanceTask 041{ 042 /** The JackRabbit RepositoryImpl */ 043 protected RepositoryImpl _repository; 044 045 @Override 046 public boolean requiresOffline() 047 { 048 return false; 049 } 050 051 @Override 052 protected void setLogger() 053 { 054 setLogger(LoggerFactory.getLogger(RemoveUnusedHistoryTask.class)); 055 } 056 057 @Override 058 protected void apply() throws RepositoryException 059 { 060 Map<String, Session> sessions = null; 061 Credentials credentials = new SimpleCredentials("ametys", new char[]{}); 062 063 try 064 { 065 _repository = getOrCreateRepository(); 066 067 String[] accessibleWorkspaceNames = _getWorkspaces(credentials); 068 sessions = _loadSessions(accessibleWorkspaceNames); 069 070 @SuppressWarnings("deprecation") 071 Query q = sessions.get("default").getWorkspace().getQueryManager().createQuery("//element(*, nt:versionHistory)", Query.XPATH); 072 NodeIterator nodes = q.execute().getNodes(); 073 long size = nodes.getSize(); 074 075 _progress = new TaskProgress(size * 2); 076 _progress.setRunning(); 077 078 List<VersionHistory> vhs = _getVersionHistoryToRemove(sessions, accessibleWorkspaceNames, nodes); 079 _progress.progress(size - vhs.size()); 080 081 _logger.info("Going to remove " + vhs.size() + " history nodes on " + size + " existing nodes"); 082 083 _removeUnusedHistory(vhs, sessions); 084 } 085 catch (RepositoryException e) 086 { 087 if (_progress != null) 088 { 089 _progress.setInErrorState(e); 090 } 091 092 throw e; 093 } 094 catch (Exception e) 095 { 096 if (_progress != null) 097 { 098 _progress.setInErrorState(e); 099 } 100 101 throw new RuntimeException(e); 102 } 103 finally 104 { 105 _logout(sessions); 106 107 if (_progress != null) 108 { 109 _progress.setFinished(); 110 } 111 112 } 113 } 114 115 private void _logout(Map<String, Session> sessions) 116 { 117 if (sessions != null) 118 { 119 for (Session session : sessions.values()) 120 { 121 session.logout(); 122 } 123 } 124 } 125 126 private String[] _getWorkspaces(Credentials credentials) throws RepositoryException 127 { 128 Session session = null; 129 try 130 { 131 session = _repository.login(credentials, "default"); 132 return session.getWorkspace().getAccessibleWorkspaceNames(); 133 } 134 finally 135 { 136 if (session != null) 137 { 138 session.logout(); 139 } 140 } 141 } 142 143 private Map<String, Session> _loadSessions(String[] accessibleWorkspaceNames) throws RepositoryException 144 { 145 Map<String, Session> sessions = new HashMap<>(); 146 Credentials credentials = new SimpleCredentials("ametys", new char[]{}); 147 148 for (String workspaceName: accessibleWorkspaceNames) 149 { 150 sessions.put(workspaceName, _repository.login(credentials, workspaceName)); 151 } 152 153 return sessions; 154 } 155 156 private void _removeUnusedHistory(List<VersionHistory> versionHistories, Map<String, Session> sessions) throws RepositoryException 157 { 158 int errors = 0; 159 int done = 0; 160 int empty = 0; 161 int fixed = 0; 162 163 for (VersionHistory versionHistory : versionHistories) 164 { 165 VersionIterator it = versionHistory.getAllVersions(); 166 167 boolean hasError = false; 168 int childNodesSize = 0; 169 170 while (it.hasNext()) 171 { 172 Version version = it.nextVersion(); 173 if (!"jcr:rootVersion".equals(version.getName())) 174 { 175 childNodesSize++; 176 177 try 178 { 179 versionHistory.removeVersion(version.getName()); 180 } 181 catch (RepositoryException e) 182 { 183 hasError = true; 184 _logger.error("Error with version " + version.getName() + " of " + versionHistory.getIdentifier() + "... " + e); 185 } 186 } 187 } 188 189 if (childNodesSize == 0) 190 { 191 _removeEmptyHistory(versionHistory, sessions.get("default")); 192 try 193 { 194 sessions.get("default").getNodeByIdentifier(versionHistory.getIdentifier()); 195 196 _logger.debug("Empty history node is " + versionHistory.getIdentifier()); 197 empty++; 198 } 199 catch (RepositoryException e) 200 { 201 fixed++; 202 } 203 } 204 else if (hasError) 205 { 206 errors++; 207 } 208 else 209 { 210 done++; 211 } 212 213 _progress.progress(); 214 } 215 216 _logger.info(done + " normal history nodes removed\n" 217 + fixed + " empty history nodes fixed and removed\n" 218 + empty + " empty history nodes that are unfixable\n" 219 + errors + " unconsistents history nodes that cannot be removed"); 220 } 221 222 private void _removeEmptyHistory(VersionHistory vh, Session session) throws RepositoryException 223 { 224 var node = ((NodeImpl) session.getNode("/ametys:root")).addNodeWithUuid("historyrepair", "nt:unstructured", vh.getProperty("jcr:versionableUuid").getString()); 225 node.addMixin("mix:versionable"); 226 session.save(); 227 session.getWorkspace().getVersionManager().checkin(node.getPath()); 228 session.getWorkspace().getVersionManager().checkout(node.getPath()); 229 session.removeItem("/ametys:root/historyrepair"); 230 session.save(); 231 vh.removeVersion("1.0"); 232 } 233 234 private List<VersionHistory> _getVersionHistoryToRemove(Map<String, Session> sessions, String[] accessibleWorkspaceNames, NodeIterator nodes) throws RepositoryException 235 { 236 List<VersionHistory> vhs = new ArrayList<>(); 237 238 while (nodes.hasNext()) 239 { 240 VersionHistory versionHistory = (VersionHistory) nodes.nextNode(); 241 String versionableIdentifier = versionHistory.getVersionableIdentifier(); 242 243 var foundOne = false; 244 for (var i = 0; i < accessibleWorkspaceNames.length; i++) 245 { 246 var workspaceName = accessibleWorkspaceNames[i]; 247 248 try 249 { 250 sessions.get(workspaceName).getNodeByIdentifier(versionableIdentifier); 251 foundOne = true; 252 break; 253 } 254 catch (RepositoryException e) 255 { 256 // nothing 257 } 258 } 259 260 if (!foundOne) 261 { 262 vhs.add(versionHistory); 263 } 264 _progress.progress(); 265 } 266 267 return vhs; 268 } 269 270}